Skip to content

Commit

Permalink
feat: New resource for individual project members
Browse files Browse the repository at this point in the history
  • Loading branch information
kusc-leica committed Feb 27, 2024
1 parent 2410db1 commit e7bd4c1
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 1 deletion.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.3.6 (to be defined)

FEATURES:

* **New Resource:** `project_member` - Separate resource to project memberships.

## 1.3.5 (Feburary 9, 2024)

BUG FIXES:
Expand Down
46 changes: 46 additions & 0 deletions docs/resources/member.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "project_member Resource - terraform-provider-project"
subcategory: ""
description: |-
Create a project member. Element has one to one mapping with the JFrog Project Users API https://jfrog.com/help/r/jfrog-rest-apis/add-or-update-user-in-project. Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if admin_privileges.manage_resoures is enabled.
---

# project_member (Resource)

Create a project member. Element has one to one mapping with the [JFrog Project Users API](https://jfrog.com/help/r/jfrog-rest-apis/add-or-update-user-in-project). Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.

## Example Usage

```terraform
resource "project_member" "myuser" {
project_key = "myproj"
name = "myuser"
roles = ["Viewer"]
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Must be existing Artifactory user
- `project_key` (String) The key of the project to which the member belongs.
- `roles` (Set of String) List of pre-defined Project or custom roles

### Optional

- `ignore_missing_user` (Boolean) When set to true, the resource will not fail if the user does not exist. Default to false. This is useful when the user is externally managed and the local account wasn't created yet.

### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import project_member.mymember project_key:username
```
1 change: 1 addition & 0 deletions examples/resources/project_member/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import project_member.mymember project_key:username
5 changes: 5 additions & 0 deletions examples/resources/project_member/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "project_member" "myuser" {
project_key = "myproj"
name = "myuser"
roles = ["Viewer"]
}
46 changes: 46 additions & 0 deletions pkg/project/project_member.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package project

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/jfrog/terraform-provider-shared/util"
)

type ProjectMember struct {
ProjectKey string `json:"-"`
Name string `json:"name"`
Roles []string `json:"roles"`
IgnoreMissingUser bool `json:"-"`
}

func unpackProjectMember(d *schema.ResourceData) ProjectMember {
return ProjectMember{
ProjectKey: d.Get("project_key").(string),
Name: d.Get("name").(string),
Roles: util.CastToStringArr(d.Get("roles").(*schema.Set).List()),
IgnoreMissingUser: d.Get("ignore_missing_user").(bool),
}
}

func packProjectMember(ctx context.Context, data *schema.ResourceData, m ProjectMember) diag.Diagnostics {
setValue := util.MkLens(data)

errors := []error{}
errors = append(errors, setValue("name", m.Name)...)
errors = append(errors, setValue("project_key", m.ProjectKey)...)
errors = append(errors, setValue("roles", m.Roles)...)
errors = append(errors, setValue("ignore_missing_user", m.IgnoreMissingUser)...)

if len(errors) > 0 {
return diag.Errorf("failed to pack project member %q", errors)
}

return nil
}

func (m ProjectMember) Id() string {
return fmt.Sprintf(`%s:%s`, m.ProjectKey, m.Name)
}
1 change: 1 addition & 0 deletions pkg/project/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func Provider() *schema.Provider {
"project": projectResource(),
"project_environment": projectEnvironmentResource(),
"project_role": projectRoleResource(),
"project_member": projectMemberResource(),
},
),
}
Expand Down
156 changes: 156 additions & 0 deletions pkg/project/resource_project_member.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package project

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/jfrog/terraform-provider-shared/util"
"github.com/jfrog/terraform-provider-shared/validator"
)

const projectMembersUrl = "access/api/v1/projects/{projectKey}/users/{name}"

func projectMemberResource() *schema.Resource {
var projectMemberSchema = map[string]*schema.Schema{
"project_key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validator.ProjectKey,
Description: "The key of the project to which the member belongs.",
},
"name": {
Type: schema.TypeString,
Required: true,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty),
Description: "Must be existing Artifactory user",
},
"roles": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "List of pre-defined Project or custom roles",
},
"ignore_missing_user": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "When set to true, the resource will not fail if the user does not exist. Default to false. This is useful when the user is externally managed and the local account wasn't created yet.",
},
}

var readProjectMember = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics {
projectMember := unpackProjectMember(data)
var loadedProjectMember ProjectMember

resp, err := m.(util.ProvderMetadata).Client.R().
SetPathParams(map[string]string{
"projectKey": projectMember.ProjectKey,
"name": projectMember.Name,
}).
SetResult(&loadedProjectMember).
Get(projectMembersUrl)

if resp != nil && resp.StatusCode() == http.StatusNotFound && projectMember.IgnoreMissingUser {
// ignore missing user, reuse local info for state
loadedProjectMember = projectMember
} else if err != nil {
return diag.FromErr(err)
}

loadedProjectMember.ProjectKey = projectMember.ProjectKey
loadedProjectMember.IgnoreMissingUser = projectMember.IgnoreMissingUser

return packProjectMember(ctx, data, loadedProjectMember)
}

var upsertProjectMember = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics {
projectMember := unpackProjectMember(data)

resp, err := m.(util.ProvderMetadata).Client.R().
SetPathParams(map[string]string{
"projectKey": projectMember.ProjectKey,
"name": projectMember.Name,
}).
SetBody(&projectMember).
Put(projectMembersUrl)

// allow missing user? -> report warning and ignore error
diagnostics := diag.Diagnostics{}

if resp != nil && resp.StatusCode() == http.StatusNotFound {
if projectMember.IgnoreMissingUser {
diagnostics = append(diagnostics, diag.Diagnostic{
Severity: diag.Warning,
Summary: fmt.Sprintf("user '%s' not found, but ignore_missing_user is set to true, project membership not created", projectMember.Name),
})
} else {
return diag.Errorf("user '%s' not found, project membership not created", projectMember.Name)
}
} else if err != nil {
return diag.FromErr(err)
}

data.SetId(projectMember.Id())

diagnostics = append(diagnostics, readProjectMember(ctx, data, m)...)

if len(diagnostics) > 0 {
return diagnostics
}

return nil
}

var deleteProjectMember = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics {
projectMember := unpackProjectMember(data)

_, err := m.(util.ProvderMetadata).Client.R().
SetPathParams(map[string]string{
"projectKey": projectMember.ProjectKey,
"name": projectMember.Name,
}).
Delete(projectMembersUrl)

if err != nil {
return diag.FromErr(err)
}

data.SetId("")

return nil
}

var importForProjectKeyMemberName = func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
parts := strings.SplitN(d.Id(), ":", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf("unexpected format of ID (%s), expected project_key:name", d.Id())
}

d.Set("project_key", parts[0])
d.Set("name", parts[1])

return []*schema.ResourceData{d}, nil
}

return &schema.Resource{
CreateContext: upsertProjectMember,
ReadContext: readProjectMember,
UpdateContext: upsertProjectMember,
DeleteContext: deleteProjectMember,

Importer: &schema.ResourceImporter{
State: importForProjectKeyMemberName,
},

Schema: projectMemberSchema,
SchemaVersion: 1,

Description: "Create a project member. Element has one to one mapping with the [JFrog Project Users API](https://jfrog.com/help/r/jfrog-rest-apis/add-or-update-user-in-project). Requires a user assigned with the 'Administer the Platform' role or Project Admin permissions if `admin_privileges.manage_resoures` is enabled.",
}
}
Loading

0 comments on commit e7bd4c1

Please sign in to comment.