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: add role attributes to launchdarkly_team_member #289

Merged
merged 39 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
67336ac
upgrade go client from v16 to v17
sloloris Feb 5, 2025
c9e3fe4
update ExperimentsBetaApi to ExperimentsApi
sloloris Feb 5, 2025
f88cd64
in data source as well
sloloris Feb 5, 2025
53d655b
fix test
sloloris Feb 5, 2025
80f2dbf
don't use foundation module
sloloris Feb 5, 2025
8c508f5
fix data source test
sloloris Feb 5, 2025
ba1aa2c
merge
sloloris Feb 5, 2025
0d498e0
run go mod tidy && go mod vendor
sloloris Feb 5, 2025
03efe70
use the non-beta client for experiments api
sloloris Feb 6, 2025
bc8db0d
make deprecated is_active field optional and computed
sloloris Feb 6, 2025
c7f2058
consolidate redundant tests
sloloris Feb 6, 2025
0953b68
Merge branch 'main' into imiller/REL-5765/upgrade-ldapi-client
sloloris Feb 6, 2025
61c8918
update tests to use a role attribute
sloloris Feb 6, 2025
2833bff
add conflicts with field to ensure policy & policy_statements do not …
sloloris Feb 6, 2025
655e41c
update description for policy_statements
sloloris Feb 6, 2025
9269319
Merge branch 'imiller/REL-5765/upgrade-ldapi-client' into imiller/REL…
sloloris Feb 6, 2025
05e8d52
add note about syntax
sloloris Feb 6, 2025
8bf18af
doc tweaks
sloloris Feb 6, 2025
83b469b
regen docs
sloloris Feb 6, 2025
00b4cbe
define role attribute schema and add to member resource
sloloris Feb 6, 2025
f7c5131
Merge branch 'main' into imiller/REL-5765/add-role-attributes-to-cust…
sloloris Feb 6, 2025
eb2a0e3
Merge branch 'imiller/REL-5765/add-role-attributes-to-custom-roles' i…
sloloris Feb 6, 2025
c975fc5
consolidate tests
sloloris Feb 6, 2025
fe26aec
update schema to specify map elems
sloloris Feb 6, 2025
52a2e6d
update scheam def
sloloris Feb 6, 2025
082ec82
add tests
sloloris Feb 6, 2025
41c43f5
add helper functions
sloloris Feb 6, 2025
0733faf
sorted but for ordering issue
sloloris Feb 6, 2025
c1cb9e9
add a patch calculation helper function and more test cases
sloloris Feb 6, 2025
87f6123
run make generate to update docs
sloloris Feb 6, 2025
dd33daa
fix nil pointer deref in helper
sloloris Feb 6, 2025
a49ea2c
Merge branch 'main' into imiller/REL-5765/add-role-attributes-to-member
sloloris Feb 6, 2025
6ba7cad
add to data source
sloloris Feb 6, 2025
adcd396
fix linter complaint
sloloris Feb 6, 2025
14254ee
missed a spot
sloloris Feb 6, 2025
5685b4f
run import state verify after every test step
sloloris Feb 6, 2025
e3a4a39
clean up comment
sloloris Feb 6, 2025
cb92a0e
cleanup
sloloris Feb 6, 2025
94f0f1f
chore: simplify roleAttributes patch logic (#291)
ldhenry Feb 6, 2025
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
12 changes: 12 additions & 0 deletions docs/data-sources/team_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,22 @@ data "launchdarkly_team_member" "example" {

- `email` (String) The unique email address associated with the team member.

### Optional

- `role_attributes` (Block Set) A role attributes block. One block must be defined per role attribute. The key is the role attribute key and the value is a string array of resource keys that apply. (see [below for nested schema](#nestedblock--role_attributes))

### Read-Only

- `custom_roles` (Set of String) The list of custom roles keys associated with the team member. Custom roles are only available to customers on an Enterprise plan. To learn more, [read about our pricing](https://launchdarkly.com/pricing/). To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/).
- `first_name` (String) The team member's given name.
- `id` (String) The 24 character alphanumeric ID of the team member.
- `last_name` (String) The team member's family name.
- `role` (String) The role associated with team member. Possible roles are `owner`, `reader`, `writer`, or `admin`.

<a id="nestedblock--role_attributes"></a>
### Nested Schema for `role_attributes`

Required:

- `key` (String) The key / name of your role attribute. In the example `$${roleAttribute/testAttribute}`, the key is `testAttribute`.
- `values` (List of String) A list of values for your role attribute. For example, if your policy statement defines the resource `"proj/$${roleAttribute/testAttribute}"`, the values would be the keys of the projects you wanted to assign access to.
9 changes: 9 additions & 0 deletions docs/data-sources/team_members.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,12 @@ Read-Only:
- `id` (String)
- `last_name` (String)
- `role` (String)
- `role_attributes` (Set of Object) (see [below for nested schema](#nestedobjatt--team_members--role_attributes))

<a id="nestedobjatt--team_members--role_attributes"></a>
### Nested Schema for `team_members.role_attributes`

Read-Only:

- `key` (String)
- `values` (List of String)
9 changes: 9 additions & 0 deletions docs/resources/team_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,20 @@ resource "launchdarkly_team_member" "example" {
- `first_name` (String) The team member's given name. Once created, this cannot be updated except by the team member.
- `last_name` (String) TThe team member's family name. Once created, this cannot be updated except by the team member.
- `role` (String) The role associated with team member. Supported roles are `reader`, `writer`, `no_access`, or `admin`. If you don't specify a role, `reader` is assigned by default.
- `role_attributes` (Block Set) A role attributes block. One block must be defined per role attribute. The key is the role attribute key and the value is a string array of resource keys that apply. (see [below for nested schema](#nestedblock--role_attributes))

### Read-Only

- `id` (String) The 24 character alphanumeric ID of the team member.

<a id="nestedblock--role_attributes"></a>
### Nested Schema for `role_attributes`

Required:

- `key` (String) The key / name of your role attribute. In the example `$${roleAttribute/testAttribute}`, the key is `testAttribute`.
- `values` (List of String) A list of values for your role attribute. For example, if your policy statement defines the resource `"proj/$${roleAttribute/testAttribute}"`, the values would be the keys of the projects you wanted to assign access to.

## Import

Import is supported using the following syntax:
Expand Down
7 changes: 6 additions & 1 deletion launchdarkly/data_source_launchdarkly_team_member.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func memberSchema() map[string]*schema.Schema {
Computed: true,
Description: `The list of custom roles keys associated with the team member. Custom roles are only available to customers on an Enterprise plan. To learn more, [read about our pricing](https://launchdarkly.com/pricing/). To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/).`,
},
ROLE_ATTRIBUTES: roleAttributesSchema(true),
}
}

Expand All @@ -63,7 +64,7 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er
teamMemberLimit := int64(1000)

// After changing this to query by member email, we shouldn't need the limit and recursion on requests, but leaving it in just to be extra safe
members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Filter(fmt.Sprintf("query:%s", url.QueryEscape(memberEmail))).Execute()
members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Filter(fmt.Sprintf("query:%s", url.QueryEscape(memberEmail))).Expand("roleAttributes").Execute()

if err != nil {
return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err))
Expand Down Expand Up @@ -111,6 +112,10 @@ func dataSourceTeamMemberRead(ctx context.Context, d *schema.ResourceData, meta
if err != nil {
return diag.Errorf("failed to set custom roles on team member with email %q: %v", member.Email, err)
}
err = d.Set(ROLE_ATTRIBUTES, roleAttributesToResourceData(member.RoleAttributes))
if err != nil {
return diag.Errorf("failed to set role attributes on team member with id %q: %v", member.Id, err)
}

return diags
}
4 changes: 4 additions & 0 deletions launchdarkly/data_source_launchdarkly_team_member_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func testAccDataSourceTeamMemberCreate(client *Client, email string) (*ldapi.Mem
Email: email,
FirstName: ldapi.PtrString("Test"),
LastName: ldapi.PtrString("Account"),
RoleAttributes: &map[string][]string{
"testAttribute": []string{"testValue"},
},
}}
members, _, err := client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm(membersBody).Execute()
if err != nil {
Expand Down Expand Up @@ -92,6 +95,7 @@ func TestAccDataSourceTeamMember_exists(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, *testMember.FirstName),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, *testMember.LastName),
resource.TestCheckResourceAttr(resourceName, ID, testMember.Id),
resource.TestCheckResourceAttr(resourceName, "role_attributes.#", "1"),
),
},
},
Expand Down
1 change: 1 addition & 0 deletions launchdarkly/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const (
RESOURCE = "resource"
RESOURCES = "resources"
ROLE = "role"
ROLE_ATTRIBUTES = "role_attributes"
ROLLOUT_CONTEXT_KIND = "rollout_context_kind"
ROLLOUT_WEIGHTS = "rollout_weights"
RULES = "rules"
Expand Down
21 changes: 15 additions & 6 deletions launchdarkly/resource_launchdarkly_team_member.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func resourceTeamMember() *schema.Resource {
Description: "The list of custom roles keys associated with the team member. Custom roles are only available to customers on an Enterprise plan. To learn more, [read about our pricing](https://launchdarkly.com/pricing/). To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/).\n\n-> **Note:** each `launchdarkly_team_member` must have either a `role` or `custom_roles` argument.",
AtLeastOneOf: []string{ROLE, CUSTOM_ROLES},
},
ROLE_ATTRIBUTES: roleAttributesSchema(false),
},

Description: `Provides a LaunchDarkly team member resource.
Expand All @@ -79,20 +80,23 @@ func resourceTeamMemberCreate(ctx context.Context, d *schema.ResourceData, metaR
lastName := d.Get(LAST_NAME).(string)
memberRole := d.Get(ROLE).(string)
customRolesRaw := d.Get(CUSTOM_ROLES).(*schema.Set).List()
roleAttributes := roleAttributesFromResourceData(d.Get(ROLE_ATTRIBUTES).(*schema.Set).List())

customRoles := make([]string, len(customRolesRaw))
for i, cr := range customRolesRaw {
customRoles[i] = cr.(string)
}

membersBody := ldapi.NewMemberForm{
Email: memberEmail,
FirstName: &firstName,
LastName: &lastName,
Role: &memberRole,
CustomRoles: customRoles,
Email: memberEmail,
FirstName: &firstName,
LastName: &lastName,
Role: &memberRole,
CustomRoles: customRoles,
RoleAttributes: roleAttributes,
}

// role attributes will not come back here because we have to set an expand query param
members, _, err := client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm([]ldapi.NewMemberForm{membersBody}).Execute()
if err != nil {
return diag.Errorf("failed to create team member with email: %s: %v", memberEmail, handleLdapiErr(err))
Expand All @@ -108,7 +112,7 @@ func resourceTeamMemberRead(ctx context.Context, d *schema.ResourceData, metaRaw
client := metaRaw.(*Client)
memberID := d.Id()

member, res, err := client.ld.AccountMembersApi.GetMember(client.ctx, memberID).Execute()
member, res, err := client.ld.AccountMembersApi.GetMember(client.ctx, memberID).Expand("roleAttributes").Execute()
if isStatusNotFound(res) {
log.Printf("[WARN] failed to find member with id %q, removing from state", memberID)
diags = append(diags, diag.Diagnostic{
Expand Down Expand Up @@ -136,6 +140,10 @@ func resourceTeamMemberRead(ctx context.Context, d *schema.ResourceData, metaRaw
if err != nil {
return diag.Errorf("failed to set custom roles on team member with id %q: %v", member.Id, err)
}
err = d.Set(ROLE_ATTRIBUTES, roleAttributesToResourceData(member.RoleAttributes))
if err != nil {
return diag.Errorf("failed to set role attributes on team member with id %q: %v", member.Id, err)
}
return diags
}

Expand All @@ -159,6 +167,7 @@ func resourceTeamMemberUpdate(ctx context.Context, d *schema.ResourceData, metaR
patchReplace("/role", &memberRole),
patchReplace("/customRoles", &customRoleIds),
}
patch = append(patch, getRoleAttributePatches(d)...)

_, _, err = client.ld.AccountMembersApi.PatchMember(client.ctx, memberID).PatchOperation(patch).Execute()
if err != nil {
Expand Down
185 changes: 158 additions & 27 deletions launchdarkly/resource_launchdarkly_team_member_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,85 @@ resource "launchdarkly_team_member" "custom_role_test" {
email = "%[email protected]"
first_name = "first"
last_name = "last"
custom_roles = [launchdarkly_custom_role.test_2.key]
custom_roles = [launchdarkly_custom_role.test.key]
}
`
testAccTeamMemberCustomRoleWithRoleAttributes = `
resource "launchdarkly_custom_role" "test" {
key = "%s"
name = "Updated - %s"
description= "Allow all actions on testAttribute environments"
policy_statements {
actions = ["*"]
effect = "allow"
resources = ["proj/*:env/$${roleAttribute/testAttribute}"]
}
}

resource "launchdarkly_team_member" "custom_role_test" {
email = "%[email protected]"
first_name = "first"
last_name = "last"
custom_roles = [launchdarkly_custom_role.test.key]
role_attributes {
key = "testAttribute"
values = ["staging", "production"]
}
role_attributes {
key = "nonexistentAttribute"
values = ["someValue"]
}
}
`
testAccTeamMemberCustomRoleWithRoleAttributesUpdate = `
resource "launchdarkly_custom_role" "test" {
key = "%s"
name = "Updated - %s"
description= "Allow all actions on testAttribute environments"
policy_statements {
actions = ["*"]
effect = "allow"
resources = ["proj/*:env/$${roleAttribute/testAttribute}"]
}
}

resource "launchdarkly_team_member" "custom_role_test" {
email = "%[email protected]"
first_name = "first"
last_name = "last"
custom_roles = [launchdarkly_custom_role.test.key]
role_attributes {
key = "newAttribute"
values = ["value1", "value2"]
}
role_attributes {
key = "testAttribute"
values = ["staging"]
}
}
`
testAccTeamMemberCustomRoleWithRoleAttributesRemove = `
resource "launchdarkly_custom_role" "test" {
key = "%s"
name = "Updated - %s"
description= "Allow all actions on testAttribute environments"
policy_statements {
actions = ["*"]
effect = "allow"
resources = ["proj/*:env/$${roleAttribute/testAttribute}"]
}
}

resource "launchdarkly_team_member" "custom_role_test" {
email = "%[email protected]"
first_name = "first"
last_name = "last"
custom_roles = [launchdarkly_custom_role.test.key]
}
`
)

func TestAccTeamMember_CreateGeneric(t *testing.T) {
func TestAccTeamMember_CreateAndUpdateGeneric(t *testing.T) {
randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
resourceName := "launchdarkly_team_member.test"
resource.ParallelTest(t, resource.TestCase{
Expand All @@ -105,30 +178,6 @@ func TestAccTeamMember_CreateGeneric(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccTeamMember_UpdateGeneric(t *testing.T) {
randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
resourceName := "launchdarkly_team_member.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccTeamMemberCreate, randomName),
Check: resource.ComposeTestCheckFunc(
testAccCheckMemberExists(resourceName),
resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%[email protected]", randomName)),
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"),
resource.TestCheckResourceAttr(resourceName, ROLE, "admin"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "0"),
),
},
{
Config: fmt.Sprintf(testAccTeamMemberUpdate, randomName),
Check: resource.ComposeTestCheckFunc(
Expand All @@ -140,6 +189,11 @@ func TestAccTeamMember_UpdateGeneric(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "0"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
Expand Down Expand Up @@ -183,7 +237,84 @@ func TestAccTeamMember_WithCustomRole(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey2),
resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey1),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
// delete launchdarkly_custom_role.test_2, udpate launchdarkly_custom_role.test with role attributes
// and add role attribute values to the team member
Config: fmt.Sprintf(testAccTeamMemberCustomRoleWithRoleAttributes, roleKey1, roleKey1, randomName),
Check: resource.ComposeTestCheckFunc(
testAccCheckCustomRoleExists(roleResourceName1),
testAccCheckMemberExists(resourceName),
resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%[email protected]", randomName)),
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey1),

resource.TestCheckResourceAttr(resourceName, "role_attributes.#", "2"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.key", "testAttribute"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.values.#", "2"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.values.0", "staging"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.values.1", "production"),
// we allow the setting of role attributes to be set even if they do not otherwise exist
// on a custom role
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.key", "nonexistentAttribute"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.values.#", "1"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.values.0", "someValue"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
// remove the nonexistentAttribute block, reorder testAttribute block and add a newAttribute block,
// and remove the production value from the testAttribute block
Config: fmt.Sprintf(testAccTeamMemberCustomRoleWithRoleAttributesUpdate, roleKey1, roleKey1, randomName),
Check: resource.ComposeTestCheckFunc(
testAccCheckCustomRoleExists(roleResourceName1),
testAccCheckMemberExists(resourceName),
resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%[email protected]", randomName)),
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey1),

resource.TestCheckResourceAttr(resourceName, "role_attributes.#", "2"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.key", "testAttribute"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.values.#", "1"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.1.values.0", "staging"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.key", "newAttribute"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.values.#", "2"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.values.0", "value1"),
resource.TestCheckResourceAttr(resourceName, "role_attributes.0.values.1", "value2"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
// remove role attributes from the team member
Config: fmt.Sprintf(testAccTeamMemberCustomRoleWithRoleAttributesRemove, roleKey1, roleKey1, randomName),
Check: resource.ComposeTestCheckFunc(
testAccCheckCustomRoleExists(roleResourceName1),
testAccCheckMemberExists(resourceName),
resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%[email protected]", randomName)),
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey1),
resource.TestCheckResourceAttr(resourceName, "role_attributes.#", "0"),
),
},
{
Expand Down
Loading