Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs/resources/team_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ resource "launchdarkly_team_member" "example" {
}
```

To adopt an existing LaunchDarkly member with the same email address instead of failing creation, set `adopt_existing = true`.

-> **Note:** When adopting an existing member, this resource updates mutable fields such as `role`, `custom_roles`, and `role_attributes`. LaunchDarkly does not allow admins to update a member's `first_name` or `last_name`.

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

Expand All @@ -36,6 +40,7 @@ resource "launchdarkly_team_member" "example" {

### Optional

- `adopt_existing` (Boolean) If true, the provider adopts an existing LaunchDarkly team member with the same email address when creation fails because the member already exists. The provider updates mutable fields after adoption, but first and last name cannot be updated except by the team member.
- `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/).

-> **Note:** each `launchdarkly_team_member` must have either a `role` or `custom_roles` argument.
Expand Down
1 change: 1 addition & 0 deletions launchdarkly/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const (
//gofmts:sort
ACTIONS = "actions"
ACTION_SET = "action_set"
ADOPT_EXISTING = "adopt_existing"
AI_CONFIG_KEY = "config_key"
ANALYSIS_TYPE = "analysis_type"
API_KEY = "api_key"
Expand Down
35 changes: 35 additions & 0 deletions launchdarkly/resource_launchdarkly_team_member.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package launchdarkly

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -37,6 +39,11 @@ func resourceTeamMember() *schema.Resource {
ForceNew: true,
Description: addForceNewDescription("The unique email address associated with the team member.", true),
},
ADOPT_EXISTING: {
Type: schema.TypeBool,
Optional: true,
Description: "If true, the provider adopts an existing LaunchDarkly team member with the same email address when creation fails because the member already exists. The provider updates mutable fields after adoption, but first and last name cannot be updated except by the team member.",
},
FIRST_NAME: {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -105,6 +112,15 @@ func resourceTeamMemberCreate(ctx context.Context, d *schema.ResourceData, metaR
return err
})
if err != nil {
if d.Get(ADOPT_EXISTING).(bool) && isTeamMemberEmailAlreadyExistsError(err) {
member, err := getTeamMemberByEmail(client, memberEmail)
if err != nil {
return diag.Errorf("failed to adopt existing team member with email: %s: %v", memberEmail, err)
}

d.SetId(member.Id)
return resourceTeamMemberUpdate(ctx, d, metaRaw)
}
return diag.Errorf("failed to create team member with email: %s: %v", memberEmail, handleLdapiErr(err))
}

Expand Down Expand Up @@ -229,3 +245,22 @@ func teamMemberExists(memberID string, client *Client) (bool, error) {

return true, nil
}

const teamMemberEmailAlreadyExistsCode = "email_already_exists_in_account"

func isTeamMemberEmailAlreadyExistsError(err error) bool {
if err == nil {
return false
}

if openAPIErr, ok := err.(*ldapi.GenericOpenAPIError); ok {
var apiErr struct {
Code string `json:"code"`
}
if json.Unmarshal(openAPIErr.Body(), &apiErr) == nil && apiErr.Code == teamMemberEmailAlreadyExistsCode {
return true
}
}

return strings.Contains(handleLdapiErr(err).Error(), teamMemberEmailAlreadyExistsCode)
}
58 changes: 58 additions & 0 deletions launchdarkly/resource_launchdarkly_team_member_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package launchdarkly

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/require"
)

const (
Expand All @@ -27,6 +29,16 @@ resource "launchdarkly_team_member" "test" {
role = "no_access"
custom_roles = []
}
`
testAccTeamMemberAdoptExisting = `
resource "launchdarkly_team_member" "test" {
email = "%s"
first_name = "Test"
last_name = "Account"
role = "no_access"
custom_roles = []
adopt_existing = true
}
`

testAccTeamMemberCustomRoleCreate = `
Expand Down Expand Up @@ -199,6 +211,52 @@ func TestAccTeamMember_CreateAndUpdateGeneric(t *testing.T) {
})
}

func TestAccTeamMember_AdoptExisting(t *testing.T) {
if os.Getenv("TF_ACC") == "" {
t.SkipNow()
}

randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
email := fmt.Sprintf("%s+wbteste2e@launchdarkly.com", randomName)
resourceName := "launchdarkly_team_member.test"

client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false, DEFAULT_HTTP_TIMEOUT_S, DEFAULT_MAX_CONCURRENCY)
require.NoError(t, err)

member, err := testAccDataSourceTeamMemberCreate(client, email)
require.NoError(t, err)
t.Cleanup(func() {
_ = testAccDataSourceTeamMemberDelete(client, member.Id)
})

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckTeamMemberDestroy,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccTeamMemberAdoptExisting, email),
Check: resource.ComposeTestCheckFunc(
testAccCheckMemberExists(resourceName),
resource.TestCheckResourceAttr(resourceName, EMAIL, email),
resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "Test"),
resource.TestCheckResourceAttr(resourceName, LAST_NAME, "Account"),
resource.TestCheckResourceAttr(resourceName, ROLE, "no_access"),
resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "0"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{ADOPT_EXISTING},
},
},
})
}

func TestAccTeamMember_WithCustomRole(t *testing.T) {
roleKey1 := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
roleKey2 := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
Expand Down