Skip to content

Commit

Permalink
Merge pull request #99 from DopplerHQ/nic/group-members
Browse files Browse the repository at this point in the history
Add doppler_group_members resource
  • Loading branch information
nmanoogian authored Aug 26, 2024
2 parents 96ccfe8 + b20456d commit bf975d4
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/resources/group_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ description: |-

Manage a Doppler user/group membership.

**Note:** You can also exclusively manage all memberships in a group with a single resource.
See the `doppler_group_members` resource for more information.

## Example Usage

```terraform
Expand Down
62 changes: 62 additions & 0 deletions docs/resources/group_members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
page_title: "doppler_group_members Resource - terraform-provider-doppler"
subcategory: ""
description: |-
Manage a Doppler group's memberships.
---

# doppler_group_members (Resource)

Manage a Doppler group's memberships.

**Note:** The `doppler_group_members` resource will clear/replace all existing memberships.
Multiple `doppler_group_members` resources or combinations of `doppler_group_members` and `doppler_group_member` will produce inconsistent behavior.
To non-exclusively manage group memberships, use `doppler_group_member` only.

## Example Usage

```terraform
resource "doppler_group" "engineering" {
name = "engineering"
}
data "doppler_user" "nic" {
email = "[email protected]"
}
data "doppler_user" "andre" {
email = "[email protected]"
}
resource "doppler_group_members" "engineering" {
group_slug = doppler_group.engineering.slug
user_slugs = [
data.doppler_user.nic.slug,
data.doppler_user.andre.slug
]
}
```

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

### Required

- `group_slug` (String) The slug of the group
- `user_slugs` (Set of String) A list of user slugs in the group

### Read-Only

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

## Import

Import is supported using the following syntax:

```shell
# import using the group slug from the URL:
# https://dashboard.doppler.com/workplace/[workplace-slug]/team/groups/[group-slug]
# and the user slugs from the URL:
# https://dashboard.doppler.com/workplace/[workplace-slug]/team/users/[user-slug]
terraform import doppler_group_members.default <group-slug>
```
36 changes: 36 additions & 0 deletions doppler/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ type QueryParam struct {
Value string
}

type PageOptions struct {
Page int
PerPage int
}

const MAX_RETRIES = 10

func (e *APIError) Error() string {
Expand Down Expand Up @@ -1185,6 +1190,37 @@ func (client APIClient) DeleteGroupMember(ctx context.Context, group string, mem
return nil
}

func (client APIClient) GetGroupMembers(ctx context.Context, group string, pageOptions PageOptions) ([]GroupMember, error) {
params := []QueryParam{
{Key: "page", Value: strconv.Itoa(pageOptions.Page)},
{Key: "per_page", Value: strconv.Itoa(pageOptions.PerPage)},
}
response, err := client.PerformRequestWithRetry(ctx, "GET", fmt.Sprintf("/v3/workplace/groups/group/%s/members", url.QueryEscape(group)), params, nil)
if err != nil {
return nil, err
}
var result GetGroupMembersResponse
if err = json.Unmarshal(response.Body, &result); err != nil {
return nil, &APIError{Err: err, Message: "Unable to parse group members"}
}
return result.Members, nil
}

func (client APIClient) ReplaceGroupMembers(ctx context.Context, group string, members []GroupMember) error {
payload := map[string]interface{}{
"members": members,
}
body, err := json.Marshal(payload)
if err != nil {
return &APIError{Err: err, Message: "Unable to serialize group members"}
}
_, err = client.PerformRequestWithRetry(ctx, "PUT", fmt.Sprintf("/v3/workplace/groups/group/%s/members", url.QueryEscape(group)), []QueryParam{}, body)
if err != nil {
return err
}
return nil
}

// Workplace Users

func (client APIClient) GetWorkplaceUser(ctx context.Context, email string) (*WorkplaceUser, error) {
Expand Down
9 changes: 9 additions & 0 deletions doppler/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ type GroupIsMemberResponse struct {
IsMember bool `json:"isMember"`
}

type GroupMember struct {
Type string `json:"type"`
Slug string `json:"slug"`
}

type GetGroupMembersResponse struct {
Members []GroupMember `json:"members"`
}

type WorkplaceUser struct {
Slug string `json:"id"`
}
Expand Down
5 changes: 3 additions & 2 deletions doppler/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ func Provider() *schema.Provider {
"doppler_service_account": resourceServiceAccount(),
"doppler_service_account_token": resourceServiceAccountToken(),

"doppler_group": resourceGroup(),
"doppler_group_member": resourceGroupMemberWorkplaceUser(),
"doppler_group": resourceGroup(),
"doppler_group_member": resourceGroupMemberWorkplaceUser(),
"doppler_group_members": resourceGroupMembers(),

"doppler_webhook": resourceWebhook(),

Expand Down
143 changes: 143 additions & 0 deletions doppler/resource_group_members.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package doppler

import (
"context"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceGroupMembers() *schema.Resource {
return &schema.Resource{
CreateContext: resourceGroupMembersCreate,
ReadContext: resourceGroupMembersRead,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
UpdateContext: resourceGroupMembersUpdate,
DeleteContext: resourceGroupMembersDelete,
Schema: map[string]*schema.Schema{
"group_slug": {
Description: "The slug of the group",
Type: schema.TypeString,
Required: true,
// Members cannot be moved directly from one group to another, they must be re-created
ForceNew: true,
},
"user_slugs": {
Description: "A list of user slugs in the group",
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
},
},
}
}

func resourceGroupMembersCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(APIClient)
var diags diag.Diagnostics

groupSlug := d.Get("group_slug").(string)
// Just fetch one member to see if any exist
currentMembers, err := client.GetGroupMembers(ctx, groupSlug, PageOptions{Page: 1, PerPage: 1})
if err != nil {
return diag.FromErr(err)
}

if len(currentMembers) > 0 {
diags = append(diags,
diag.Diagnostic{
Severity: diag.Warning,
Summary: "This group has existing members",
Detail: "This group has existing members. All group memberships have been overwritten by this resource.",
})
}

diags = append(diags, resourceGroupMembersUpdate(ctx, d, m)...)

d.SetId(groupSlug)

return diags
}

func resourceGroupMembersUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(APIClient)

var diags diag.Diagnostics
groupSlug := d.Get("group_slug").(string)
userSlugs := d.Get("user_slugs").(*schema.Set).List()

members := make([]GroupMember, len(userSlugs))
for i, v := range userSlugs {
members[i] = GroupMember{Type: "workplace_user", Slug: v.(string)}
}

err := client.ReplaceGroupMembers(ctx, groupSlug, members)
if err != nil {
return diag.FromErr(err)
}

return diags
}

func resourceGroupMembersRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(APIClient)

var diags diag.Diagnostics

groupSlug := d.Id()

perPage := 1000
maxPages := 5

members := []GroupMember{}

for page := 1; page <= maxPages; page++ {
pageMembers, err := client.GetGroupMembers(ctx, groupSlug, PageOptions{Page: page, PerPage: perPage})
if err != nil {
return handleNotFoundError(err, d)
}
members = append(members, pageMembers...)
if len(pageMembers) < perPage {
break
} else if page == maxPages {
return diag.Errorf("Exceeded max number of group members")
}
}

userSlugs := []string{}
for _, v := range members {
if v.Type == "workplace_user" {
userSlugs = append(userSlugs, v.Slug)
} else {
return diag.Errorf("Actor type %s is not supported by this plugin version", v.Type)
}
}

if err := d.Set("group_slug", groupSlug); err != nil {
return diag.FromErr((err))
}

if err := d.Set("user_slugs", userSlugs); err != nil {
return diag.FromErr((err))
}

return diags
}

func resourceGroupMembersDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(APIClient)

var diags diag.Diagnostics
groupSlug := d.Id()

// Setting the members to an empty list effectively deletes the memberships
if err := client.ReplaceGroupMembers(ctx, groupSlug, []GroupMember{}); err != nil {
return diag.FromErr(err)
}

return diags
}
20 changes: 20 additions & 0 deletions examples/resources/group_members.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
resource "doppler_group" "engineering" {
name = "engineering"
}

data "doppler_user" "nic" {
email = "[email protected]"
}

data "doppler_user" "andre" {
email = "[email protected]"
}

resource "doppler_group_members" "engineering" {
group_slug = doppler_group.engineering.slug
user_slugs = [
data.doppler_user.nic.slug,
data.doppler_user.andre.slug
]
}

3 changes: 3 additions & 0 deletions templates/resources/group_member.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ description: |-

Manage a Doppler user/group membership.

**Note:** You can also exclusively manage all memberships in a group with a single resource.
See the `doppler_group_members` resource for more information.

## Example Usage

{{tffile "examples/resources/group_member.tf"}}
Expand Down
32 changes: 32 additions & 0 deletions templates/resources/group_members.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
page_title: "doppler_group_members Resource - terraform-provider-doppler"
subcategory: ""
description: |-
Manage a Doppler group's memberships.
---

# doppler_group_members (Resource)

Manage a Doppler group's memberships.

**Note:** The `doppler_group_members` resource will clear/replace all existing memberships.
Multiple `doppler_group_members` resources or combinations of `doppler_group_members` and `doppler_group_member` will produce inconsistent behavior.
To non-exclusively manage group memberships, use `doppler_group_member` only.

## Example Usage

{{tffile "examples/resources/group_members.tf"}}

{{ .SchemaMarkdown | trimspace }}

## Import

Import is supported using the following syntax:

```shell
# import using the group slug from the URL:
# https://dashboard.doppler.com/workplace/[workplace-slug]/team/groups/[group-slug]
# and the user slugs from the URL:
# https://dashboard.doppler.com/workplace/[workplace-slug]/team/users/[user-slug]
terraform import doppler_group_members.default <group-slug>
```

0 comments on commit bf975d4

Please sign in to comment.