Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New resource for individual project members #99

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading