From bce5a6b161aef25c8079dd355faeb3fd1ae43d20 Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Tue, 22 Apr 2025 10:50:47 -0500 Subject: [PATCH 1/8] Upgrade go-tfe --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 67c731be1..b7ac18238 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-slug v0.16.5 - github.com/hashicorp/go-tfe v1.78.0 + github.com/hashicorp/go-tfe v1.78.1-0.20250418170002-da71abb96c5a github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.23.0 // indirect diff --git a/go.sum b/go.sum index fc405c46f..8e8764d98 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-slug v0.16.5 h1:L3frr/NptQ9/HLlIWoTmv4tx3Lh+Gx0o5XINGet3cN0= github.com/hashicorp/go-slug v0.16.5/go.mod h1:X5fm++dL59cDOX8j48CqHr4KARTQau7isGh0ZVxJB5I= -github.com/hashicorp/go-tfe v1.78.0 h1:RMkrEO3N4hbnXqoMWl44TnSCkMXpON5iEOOJf+UxWAo= -github.com/hashicorp/go-tfe v1.78.0/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA= +github.com/hashicorp/go-tfe v1.78.1-0.20250418170002-da71abb96c5a h1:NOAwqEnCuXNqMNdeFRV4NxEutcmxGE1/3ML0AbXQ15I= +github.com/hashicorp/go-tfe v1.78.1-0.20250418170002-da71abb96c5a/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= From 85bcb3bb828f5ca046a0f64492b4f9becb6c8b52 Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Tue, 22 Apr 2025 10:50:47 -0500 Subject: [PATCH 2/8] Add description as option for team token By setting the description, this allows for creation of multiple team tokens. --- internal/provider/resource_tfe_team_token.go | 54 ++++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/internal/provider/resource_tfe_team_token.go b/internal/provider/resource_tfe_team_token.go index ecc050666..14e26c86f 100644 --- a/internal/provider/resource_tfe_team_token.go +++ b/internal/provider/resource_tfe_team_token.go @@ -10,12 +10,15 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -39,6 +42,7 @@ type modelTFETeamToken struct { ForceRegenerate types.Bool `tfsdk:"force_regenerate"` Token types.String `tfsdk:"token"` ExpiredAt types.String `tfsdk:"expired_at"` + Description types.String `tfsdk:"description"` } func (r *resourceTFETeamToken) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -83,6 +87,9 @@ func (r *resourceTFETeamToken) Schema(_ context.Context, _ resource.SchemaReques PlanModifiers: []planmodifier.Bool{ boolplanmodifier.RequiresReplace(), }, + Validators: []validator.Bool{ + boolvalidator.ConflictsWith(path.MatchRoot("description")), + }, }, "token": schema.StringAttribute{ Description: "The generated token.", @@ -96,8 +103,18 @@ func (r *resourceTFETeamToken) Schema(_ context.Context, _ resource.SchemaReques stringplanmodifier.RequiresReplace(), }, }, + "description": schema.StringAttribute{ + Description: "The description of the token, which must be unique per team.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("force_regenerate")), + }, + }, }, - Description: "Generates a new team token and overrides existing token if one exists.", + Description: "Generates a new team token. If no description is provided, it follows the legacy behavior to override the existing, descriptionless token if one exists.", } } @@ -110,28 +127,33 @@ func (r *resourceTFETeamToken) Create(ctx context.Context, req resource.CreateRe } teamID := plan.TeamID.ValueString() - tflog.Debug(ctx, fmt.Sprintf("Check if a token already exists for team: %s", teamID)) - _, err := r.config.Client.TeamTokens.Read(ctx, teamID) - if err != nil && !errors.Is(err, tfe.ErrResourceNotFound) { - resp.Diagnostics.AddError( - fmt.Sprintf("Error checking if a token exists for team %s", teamID), - err.Error(), - ) - return - } - if err == nil { - if !plan.ForceRegenerate.ValueBool() { + if plan.Description.IsNull() { + // No description indicates legacy behavior where token will be regenerated if it does not exist + tflog.Debug(ctx, fmt.Sprintf("Check if a token already exists for team: %s", teamID)) + _, err := r.config.Client.TeamTokens.Read(ctx, teamID) + if err != nil && !errors.Is(err, tfe.ErrResourceNotFound) { resp.Diagnostics.AddError( - fmt.Sprintf("A token already exists for team: %s", teamID), - "Set force_regenerate to true to regenerate the token.", + fmt.Sprintf("Error checking if a token exists for team %s", teamID), + err.Error(), ) return } - tflog.Debug(ctx, fmt.Sprintf("Regenerating existing token for team: %s", teamID)) + if err == nil { + if !plan.ForceRegenerate.ValueBool() { + resp.Diagnostics.AddError( + fmt.Sprintf("A token already exists for team: %s", teamID), + "Set force_regenerate to true to regenerate the token.", + ) + return + } + tflog.Debug(ctx, fmt.Sprintf("Regenerating existing token for team: %s", teamID)) + } } expiredAt := plan.ExpiredAt.ValueString() - options := tfe.TeamTokenCreateOptions{} + options := tfe.TeamTokenCreateOptions{ + Description: plan.Description.ValueStringPointer(), + } if !plan.ExpiredAt.IsNull() && expiredAt != "" { expiry, err := time.Parse(time.RFC3339, expiredAt) if err != nil { From dd52671a91eba2724bea9e4ccf39cce8823238c2 Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Tue, 22 Apr 2025 10:50:47 -0500 Subject: [PATCH 3/8] Use token ID as resource ID when multiple tokens Previously, when a team only had a single token, it was sufficient to have the ID of the token be set to the team ID. Now that we support multiple team tokens, we should use the token ID instead. We will continue to use the team ID for descriptionless tokens so that it is backwards compatible, though. --- internal/provider/resource_tfe_team_token.go | 37 +++++- .../provider/resource_tfe_team_token_test.go | 107 +++++++++++++++++- 2 files changed, 136 insertions(+), 8 deletions(-) diff --git a/internal/provider/resource_tfe_team_token.go b/internal/provider/resource_tfe_team_token.go index 14e26c86f..ffb6d1f18 100644 --- a/internal/provider/resource_tfe_team_token.go +++ b/internal/provider/resource_tfe_team_token.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "strings" "time" tfe "github.com/hashicorp/go-tfe" @@ -175,22 +176,29 @@ func (r *resourceTFETeamToken) Create(ctx context.Context, req resource.CreateRe return } - result := modelFromTFEToken(plan.TeamID, types.StringValue(token.Token), plan.ForceRegenerate, plan.ExpiredAt) + result := modelFromTFEToken(plan.TeamID, types.StringValue(token.ID), types.StringValue(token.Token), plan.ForceRegenerate, plan.ExpiredAt, plan.Description) resp.Diagnostics.Append(resp.State.Set(ctx, result)...) } -func modelFromTFEToken(teamID types.String, stateValue types.String, forceRegenerate types.Bool, expiredAt types.String) modelTFETeamToken { +func modelFromTFEToken(teamID types.String, tokenID types.String, stateValue types.String, forceRegenerate types.Bool, expiredAt types.String, description types.String) modelTFETeamToken { m := modelTFETeamToken{ - ID: teamID, TeamID: teamID, ForceRegenerate: forceRegenerate, ExpiredAt: types.StringNull(), Token: stateValue, + Description: types.StringNull(), } if !expiredAt.IsNull() { m.ExpiredAt = expiredAt } + if !description.IsNull() { + m.Description = description + m.ID = tokenID + } else { + m.ID = teamID + } + return m } @@ -204,7 +212,12 @@ func (r *resourceTFETeamToken) Read(ctx context.Context, req resource.ReadReques teamID := state.TeamID.ValueString() tflog.Debug(ctx, fmt.Sprintf("Read the token from team: %s", teamID)) - _, err := r.config.Client.TeamTokens.Read(ctx, teamID) + var err error + if isTokenID(state.ID.ValueString()) { + _, err = r.config.Client.TeamTokens.ReadByID(ctx, state.ID.ValueString()) + } else { + _, err = r.config.Client.TeamTokens.Read(ctx, teamID) + } if err != nil { if errors.Is(err, tfe.ErrResourceNotFound) { tflog.Debug(ctx, fmt.Sprintf("Token for team %s no longer exists", teamID)) @@ -217,7 +230,7 @@ func (r *resourceTFETeamToken) Read(ctx context.Context, req resource.ReadReques ) return } - result := modelFromTFEToken(state.TeamID, state.Token, state.ForceRegenerate, state.ExpiredAt) + result := modelFromTFEToken(state.TeamID, state.ID, state.Token, state.ForceRegenerate, state.ExpiredAt, state.Description) resp.Diagnostics.Append(resp.State.Set(ctx, result)...) } @@ -236,7 +249,13 @@ func (r *resourceTFETeamToken) Delete(ctx context.Context, req resource.DeleteRe teamID := state.TeamID.ValueString() tflog.Debug(ctx, fmt.Sprintf("Delete the token from team: %s", teamID)) - if err := r.config.Client.TeamTokens.Delete(ctx, teamID); err != nil { + var err error + if isTokenID(state.ID.ValueString()) { + err = r.config.Client.TeamTokens.DeleteByID(ctx, state.ID.ValueString()) + } else { + err = r.config.Client.TeamTokens.Delete(ctx, teamID) + } + if err != nil { if errors.Is(err, tfe.ErrResourceNotFound) { tflog.Debug(ctx, fmt.Sprintf("Token for team %s no longer exists", teamID)) return @@ -251,3 +270,9 @@ func (r *resourceTFETeamToken) Delete(ctx context.Context, req resource.DeleteRe func (r *resourceTFETeamToken) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("team_id"), req, resp) } + +// Determines whether the ID of the resource is the ID of the authentication token +// or the ID of the team the token belongs to. +func isTokenID(id string) bool { + return strings.HasPrefix(id, "at-") +} diff --git a/internal/provider/resource_tfe_team_token_test.go b/internal/provider/resource_tfe_team_token_test.go index 563677d1d..7c6f5ba33 100644 --- a/internal/provider/resource_tfe_team_token_test.go +++ b/internal/provider/resource_tfe_team_token_test.go @@ -35,6 +35,31 @@ func TestAccTFETeamToken_basic(t *testing.T) { }) } +func TestAccTFETeamToken_multiple_team_tokens(t *testing.T) { + skipUnlessBeta(t) + token := &tfe.TeamToken{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamTokenDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamToken_withMultipleTokens(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamTokenExists( + "tfe_team_token.multi_token_1", token), + testAccCheckTFETeamTokenExists( + "tfe_team_token.multi_token_2", token), + testAccCheckTFETeamTokenExists( + "tfe_team_token.legacy", token), + ), + }, + }, + }) +} + func TestAccTFETeamToken_existsWithoutForce(t *testing.T) { token := &tfe.TeamToken{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -88,6 +113,23 @@ func TestAccTFETeamToken_existsWithForce(t *testing.T) { }) } +func TestAccTFETeamToken_invalidWithForceGenerateAndDescription(t *testing.T) { + skipUnlessBeta(t) + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamTokenDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamToken_WithForceGenerateAndDescription(rInt), + ExpectError: regexp.MustCompile(`"force_regenerate" cannot be specified when "description"`), + }, + }, + }) +} + func TestAccTFETeamToken_withBlankExpiry(t *testing.T) { token := &tfe.TeamToken{} rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -184,7 +226,14 @@ func testAccCheckTFETeamTokenExists( return fmt.Errorf("No instance ID is set") } - tt, err := testAccConfiguredClient.Client.TeamTokens.Read(ctx, rs.Primary.ID) + var tt *tfe.TeamToken + var err error + if isTokenID(rs.Primary.ID) { + tt, err = testAccConfiguredClient.Client.TeamTokens.ReadByID(ctx, rs.Primary.ID) + } else { + tt, err = testAccConfiguredClient.Client.TeamTokens.Read(ctx, rs.Primary.ID) + } + if err != nil { return err } @@ -209,7 +258,12 @@ func testAccCheckTFETeamTokenDestroy(s *terraform.State) error { return fmt.Errorf("No instance ID is set") } - _, err := testAccConfiguredClient.Client.TeamTokens.Read(ctx, rs.Primary.ID) + var err error + if isTokenID(rs.Primary.ID) { + _, err = testAccConfiguredClient.Client.TeamTokens.ReadByID(ctx, rs.Primary.ID) + } else { + _, err = testAccConfiguredClient.Client.TeamTokens.Read(ctx, rs.Primary.ID) + } if err == nil { return fmt.Errorf("Team token %s still exists", rs.Primary.ID) } @@ -336,3 +390,52 @@ resource "tfe_team_token" "expiry" { expired_at = "2000-04-11" }`, rInt) } + +func testAccTFETeamToken_withMultipleTokens(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = tfe_organization.foobar.id +} + + +resource "tfe_team_token" "multi_token_1" { + team_id = tfe_team.foobar.id + description = "tst-terraform-%d-token-1" + expired_at = "2051-04-11T23:15:59Z" +} + +resource "tfe_team_token" "multi_token_2" { + team_id = tfe_team.foobar.id + description = "tst-terraform-%d-token-2" +} + +resource "tfe_team_token" "legacy" { + team_id = tfe_team.foobar.id +}`, rInt, rInt, rInt) +} + +func testAccTFETeamToken_WithForceGenerateAndDescription(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = tfe_organization.foobar.id +} + + +resource "tfe_team_token" "invalid" { + team_id = tfe_team.foobar.id + description = "tst-terraform-%d-token" + force_regenerate = true +}`, rInt, rInt) +} From 12ef62ab8581665e219a9473dabf3ea75eccecc5 Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Tue, 22 Apr 2025 10:50:47 -0500 Subject: [PATCH 4/8] Support importing by team token ID --- internal/provider/resource_tfe_team_token.go | 35 ++++++++++++++++++- .../provider/resource_tfe_team_token_test.go | 34 ++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/internal/provider/resource_tfe_team_token.go b/internal/provider/resource_tfe_team_token.go index ffb6d1f18..a4bccf851 100644 --- a/internal/provider/resource_tfe_team_token.go +++ b/internal/provider/resource_tfe_team_token.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -268,7 +269,39 @@ func (r *resourceTFETeamToken) Delete(ctx context.Context, req resource.DeleteRe } func (r *resourceTFETeamToken) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("team_id"), req, resp) + if !isTokenID(req.ID) { + // Set the team ID field + resource.ImportStatePassthroughID(ctx, path.Root("team_id"), req, resp) + return + } + + // Fetch token by ID to set attributes + token, err := r.config.Client.TeamTokens.ReadByID(ctx, req.ID) + if err != nil { + resp.Diagnostics.AddError("Error importing team token", err.Error()) + return + } + if token.Team == nil { + resp.Diagnostics.AddError("Error importing team token", "token did not return associated team") + return + } + + var expiredAt types.String + if !token.ExpiredAt.IsZero() { + expiredAt = types.StringValue(token.ExpiredAt.Format(time.RFC3339)) + } else { + expiredAt = types.StringNull() + } + + var description types.String + if token.Description != nil { + description = types.StringValue(*token.Description) + } else { + description = types.StringNull() + } + + result := modelFromTFEToken(types.StringValue(token.Team.ID), types.StringValue(token.ID), types.StringValue(token.Token), basetypes.NewBoolNull(), expiredAt, description) + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) } // Determines whether the ID of the resource is the ID of the authentication token diff --git a/internal/provider/resource_tfe_team_token_test.go b/internal/provider/resource_tfe_team_token_test.go index 7c6f5ba33..f1bc7eda2 100644 --- a/internal/provider/resource_tfe_team_token_test.go +++ b/internal/provider/resource_tfe_team_token_test.go @@ -214,6 +214,40 @@ func TestAccTFETeamToken_import(t *testing.T) { }) } +func TestAccTFETeamToken_importByTokenID(t *testing.T) { + skipUnlessBeta(t) + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamTokenDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamToken_withMultipleTokens(rInt), + }, + { + ResourceName: "tfe_team_token.multi_token_1", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + { + ResourceName: "tfe_team_token.multi_token_2", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + { + ResourceName: "tfe_team_token.legacy", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + }, + }) +} + func testAccCheckTFETeamTokenExists( n string, token *tfe.TeamToken) resource.TestCheckFunc { return func(s *terraform.State) error { From 1954baa09d878e3089268bf9265810cad6070827 Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Tue, 22 Apr 2025 10:50:47 -0500 Subject: [PATCH 5/8] Update documentation for team tokens --- website/docs/r/team_token.html.markdown | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/website/docs/r/team_token.html.markdown b/website/docs/r/team_token.html.markdown index 42c563191..8913e7b79 100644 --- a/website/docs/r/team_token.html.markdown +++ b/website/docs/r/team_token.html.markdown @@ -7,7 +7,8 @@ description: |- # tfe_team_token -Generates a new team token and overrides existing token if one exists. +Generates a new team token. If a description is not set, then it follows the legacy behavior to override +the single team token without a description if it exists. ## Example Usage @@ -20,7 +21,13 @@ resource "tfe_team" "test" { } resource "tfe_team_token" "test" { - team_id = tfe_team.test.id + team_id = tfe_team.test.id + description = "my team token" +} + +resource "tfe_team_token" "ci" { + team_id = tfe_team.test.id + description = "my second team token" } ``` @@ -29,12 +36,14 @@ resource "tfe_team_token" "test" { The following arguments are supported: * `team_id` - (Required) ID of the team. -* `force_regenerate` - (Optional) If set to `true`, a new token will be - generated even if a token already exists. This will invalidate the existing - token! +* `description` - (Optional) The token's description, which must be unique per team. Required if creating multiple + tokens for a single team. * `expired_at` - (Optional) The token's expiration date. The expiration date must be a date/time string in RFC3339 format (e.g., "2024-12-31T23:59:59Z"). If no expiration date is supplied, the expiration date will default to null and never expire. +* `force_regenerate` - (Optional) Only applies to legacy tokens without descriptions. If set to `true`, a new + token will be generated even if a token already exists. This will invalidate the existing token! This cannot + be set with `description`. ## Example Usage @@ -52,6 +61,7 @@ resource "time_rotating" "example" { resource "tfe_team_token" "test" { team_id = tfe_team.test.id + description = "my team token" expired_at = time_rotating.example.rotation_rfc3339 } ``` @@ -63,8 +73,12 @@ resource "tfe_team_token" "test" { ## Import -Team tokens can be imported; use `` as the import ID. For example: +Team tokens can be imported either by `` or by ``. Using the team ID will follow the +legacy behavior where the imported token is the single token of the team that has no description. + +For example: ```shell +terraform import tfe_team_token.test at-47qC3LmA47piVan7 terraform import tfe_team_token.test team-47qC3LmA47piVan7 ``` From bdc8aece11cf81cc0bea34b181d3c1b394194cfd Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Tue, 22 Apr 2025 10:50:47 -0500 Subject: [PATCH 6/8] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e93c27617..092f701c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ BUG FIXES: * `d/tfe_outputs`: fixes 'error inferring type for key' for output objects that had a key with null value. (#1709), by @uturunku1 [#1699](https://github.com/hashicorp/terraform-provider-tfe/pull/1709) +FEATURES: + +* `r/tfe_team_token`: Adds support for creating multiple team tokens for a single team by adding the `description` attribute, which must be unique per team, by @mkam [#1698](https://github.com/hashicorp/terraform-provider-tfe/pull/1698) + ## v0.65.2 BUG FIXES: From 7e23e927a1cbba2310a072293eb9e0cf70b3fa35 Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Wed, 7 May 2025 15:52:09 -0500 Subject: [PATCH 7/8] Update go-tfe to v1.79.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b7ac18238..f3940ad33 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-slug v0.16.5 - github.com/hashicorp/go-tfe v1.78.1-0.20250418170002-da71abb96c5a + github.com/hashicorp/go-tfe v1.79.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.23.0 // indirect diff --git a/go.sum b/go.sum index 8e8764d98..a11347f81 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-slug v0.16.5 h1:L3frr/NptQ9/HLlIWoTmv4tx3Lh+Gx0o5XINGet3cN0= github.com/hashicorp/go-slug v0.16.5/go.mod h1:X5fm++dL59cDOX8j48CqHr4KARTQau7isGh0ZVxJB5I= -github.com/hashicorp/go-tfe v1.78.1-0.20250418170002-da71abb96c5a h1:NOAwqEnCuXNqMNdeFRV4NxEutcmxGE1/3ML0AbXQ15I= -github.com/hashicorp/go-tfe v1.78.1-0.20250418170002-da71abb96c5a/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA= +github.com/hashicorp/go-tfe v1.79.0 h1:9V48ssu+foL3+wB7+5/FC5wOWH1TiRoHMdpW1w6zynM= +github.com/hashicorp/go-tfe v1.79.0/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= From 824606fd483f711e62c83d64f7fcea17ca343c7a Mon Sep 17 00:00:00 2001 From: Melissa Kam Date: Mon, 12 May 2025 14:18:55 -0500 Subject: [PATCH 8/8] Clarify that create error can be due to TFE version --- internal/provider/resource_tfe_team_token.go | 7 ++++++- .../provider/resource_tfe_team_token_test.go | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/internal/provider/resource_tfe_team_token.go b/internal/provider/resource_tfe_team_token.go index a4bccf851..fa15e75b5 100644 --- a/internal/provider/resource_tfe_team_token.go +++ b/internal/provider/resource_tfe_team_token.go @@ -170,9 +170,14 @@ func (r *resourceTFETeamToken) Create(ctx context.Context, req resource.CreateRe token, err := r.config.Client.TeamTokens.CreateWithOptions(ctx, teamID, options) if err != nil { + errDetails := err.Error() + if errors.Is(err, tfe.ErrResourceNotFound) { + errDetails = fmt.Sprintf("%s, team does not exist or version of Terraform Enterprise "+ + "does not support multiple team tokens with descriptions", errDetails) + } resp.Diagnostics.AddError( fmt.Sprintf("Error creating new token for team %s", teamID), - err.Error(), + errDetails, ) return } diff --git a/internal/provider/resource_tfe_team_token_test.go b/internal/provider/resource_tfe_team_token_test.go index f1bc7eda2..0511f4e14 100644 --- a/internal/provider/resource_tfe_team_token_test.go +++ b/internal/provider/resource_tfe_team_token_test.go @@ -248,6 +248,24 @@ func TestAccTFETeamToken_importByTokenID(t *testing.T) { }) } +func TestAccTFETeamToken_withNonexistentTeam(t *testing.T) { + conf := ` +resource "tfe_team_token" "invalid" { + team_id = "invalid" +}` + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamTokenDestroy, + Steps: []resource.TestStep{ + { + Config: conf, + ExpectError: regexp.MustCompile("resource not found, team does not exist or version of Terraform Enterprise\ndoes not support multiple team tokens with descriptions"), + }, + }, + }) +} + func testAccCheckTFETeamTokenExists( n string, token *tfe.TeamToken) resource.TestCheckFunc { return func(s *terraform.State) error {