diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ff10089..0787cf9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +BUG FIXES: +* `r/tfe_workspace_settings`: Prevent unintended clearing of workspace-level tags on the first apply when tags is unset by making tag updates sparse. By @shwetamurali [#1851](https://github.com/hashicorp/terraform-provider-tfe/pull/1851) + ## v0.69.0 BREAKING CHANGES: diff --git a/internal/provider/resource_tfe_workspace_settings.go b/internal/provider/resource_tfe_workspace_settings.go index b6fd85fd0..8ac83337e 100644 --- a/internal/provider/resource_tfe_workspace_settings.go +++ b/internal/provider/resource_tfe_workspace_settings.go @@ -513,20 +513,24 @@ func (r *workspaceSettings) updateSettings(ctx context.Context, data *modelWorks updateOptions.ExecutionMode = tfe.String("remote") } - tags := data.Tags.Elements() - for key, val := range tags { - if strVal, ok := val.(types.String); ok && !strVal.IsNull() { - updateOptions.TagBindings = append(updateOptions.TagBindings, &tfe.TagBinding{ - Key: key, - Value: strVal.ValueString(), - }) - } - } - - if len(tags) == 0 { - err := r.config.Client.Workspaces.DeleteAllTagBindings(ctx, workspaceID) - if err != nil { - return fmt.Errorf("error removing tag bindings from workspace %s: %w", workspaceID, err) + if !data.Tags.IsNull() && !data.Tags.IsUnknown() { + tags := data.Tags.Elements() + + switch { + case len(tags) == 0: + if err := r.config.Client.Workspaces.DeleteAllTagBindings(ctx, workspaceID); err != nil { + return fmt.Errorf("error removing tag bindings from workspace %s: %w", workspaceID, err) + } + default: + for key, val := range tags { + strVal, ok := val.(types.String) + if ok && !strVal.IsNull() && !strVal.IsUnknown() { + updateOptions.TagBindings = append(updateOptions.TagBindings, &tfe.TagBinding{ + Key: key, + Value: strVal.ValueString(), + }) + } + } } } diff --git a/internal/provider/resource_tfe_workspace_settings_test.go b/internal/provider/resource_tfe_workspace_settings_test.go index b266366f5..e854fc60a 100644 --- a/internal/provider/resource_tfe_workspace_settings_test.go +++ b/internal/provider/resource_tfe_workspace_settings_test.go @@ -594,3 +594,121 @@ resource "tfe_workspace_settings" "test" { } ` } + +func TestAccTFEWorkspaceSettings_preservesWorkspaceTagsOnFirstApply(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + configStep := fmt.Sprintf(` +resource "tfe_organization" "test" { + name = "tst-tfeprovider-%d" + email = "admin@company.com" +} + +resource "tfe_project" "test" { + organization = tfe_organization.test.name + name = "tfe-provider-test-%d" + tags = { projectTag = "valueA" } +} + +resource "tfe_workspace" "test" { + name = "tfe-provider-test-workspace-%d" + organization = tfe_organization.test.name + project_id = tfe_project.test.id + tags = { app = "web" } # workspace-level tag +} + +resource "tfe_workspace_settings" "test" { + workspace_id = tfe_workspace.test.id +} +`, rInt, rInt, rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: configStep, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.%", "2"), + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.projectTag", "valueA"), + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.app", "web"), + ), + }, + }, + }) +} + +func TestAccTFEWorkspaceSettings_explicitEmptyClearsWorkspaceTags(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + configStep1 := fmt.Sprintf(` +resource "tfe_organization" "test" { + name = "tst-tfeprovider-%d" + email = "admin@company.com" +} + +resource "tfe_project" "test" { + organization = tfe_organization.test.name + name = "tfe-provider-test-%d" + tags = { projectTag = "valueA" } +} + +resource "tfe_workspace" "test" { + name = "tfe-provider-test-workspace-%d" + organization = tfe_organization.test.name + project_id = tfe_project.test.id + tags = { app = "web" } # workspace-level tag +} + +resource "tfe_workspace_settings" "test" { + workspace_id = tfe_workspace.test.id +} +`, rInt, rInt, rInt) + + configStep2 := fmt.Sprintf(` +resource "tfe_organization" "test" { + name = "tst-tfeprovider-%d" + email = "admin@company.com" +} + +resource "tfe_project" "test" { + organization = tfe_organization.test.name + name = "tfe-provider-test-%d" + tags = { projectTag = "valueA" } +} + +resource "tfe_workspace" "test" { + name = "tfe-provider-test-workspace-%d" + organization = tfe_organization.test.name + project_id = tfe_project.test.id +} + +resource "tfe_workspace_settings" "test" { + workspace_id = tfe_workspace.test.id + tags = {} +} +`, rInt, rInt, rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: configStep1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.%", "2"), + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.projectTag", "valueA"), + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.app", "web"), + ), + }, + { + Config: configStep2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.%", "1"), + resource.TestCheckResourceAttr("tfe_workspace_settings.test", "effective_tags.projectTag", "valueA"), + resource.TestCheckNoResourceAttr("tfe_workspace_settings.test", "effective_tags.app"), + ), + }, + }, + }) +}