From 1da7031855b0089ae067e4456738dd64f21973ba Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Mon, 1 Nov 2021 20:42:54 -0400 Subject: [PATCH] cloud: Add tags to workspace if necessary when fetching state --- internal/cloud/backend.go | 53 +++++++-- .../e2e/migrate_state_multi_to_tfc_test.go | 103 ++++++++++++++++++ internal/cloud/tfe_client_mock.go | 2 +- 3 files changed, 150 insertions(+), 8 deletions(-) diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index 90bce8fb6..cff9d7e2b 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -529,15 +529,9 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { // Create a workspace options := tfe.WorkspaceCreateOptions{ Name: tfe.String(name), + Tags: b.WorkspaceMapping.tfeTags(), } - var tags []*tfe.Tag - for _, tag := range b.WorkspaceMapping.Tags { - t := tfe.Tag{Name: tag} - tags = append(tags, &t) - } - options.Tags = tags - log.Printf("[TRACE] cloud: Creating Terraform Cloud workspace %s/%s", b.organization, name) workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options) if err != nil { @@ -569,6 +563,17 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { } } + if b.workspaceTagsRequireUpdate(workspace, b.WorkspaceMapping) { + options := tfe.WorkspaceAddTagsOptions{ + Tags: b.WorkspaceMapping.tfeTags(), + } + log.Printf("[TRACE] cloud: Adding tags for Terraform Cloud workspace %s/%s", b.organization, name) + err = b.client.Workspaces.AddTags(context.Background(), workspace.ID, options) + if err != nil { + return nil, fmt.Errorf("Error updating workspace %s: %v", name, err) + } + } + // This is a fallback error check. Most code paths should use other // mechanisms to check the version, then set the ignoreVersionConflict // field to true. This check is only in place to ensure that we don't @@ -913,6 +918,25 @@ func (b *Cloud) cliColorize() *colorstring.Colorize { } } +func (b *Cloud) workspaceTagsRequireUpdate(workspace *tfe.Workspace, workspaceMapping WorkspaceMapping) bool { + if workspaceMapping.Strategy() != WorkspaceTagsStrategy { + return false + } + + existingTags := map[string]struct{}{} + for _, t := range workspace.TagNames { + existingTags[t] = struct{}{} + } + + for _, tag := range workspaceMapping.Tags { + if _, ok := existingTags[tag]; !ok { + return true + } + } + + return false +} + type WorkspaceMapping struct { Name string Tags []string @@ -941,6 +965,21 @@ func (wm WorkspaceMapping) Strategy() workspaceStrategy { } } +func (wm WorkspaceMapping) tfeTags() []*tfe.Tag { + var tags []*tfe.Tag + + if wm.Strategy() != WorkspaceTagsStrategy { + return tags + } + + for _, tag := range wm.Tags { + t := tfe.Tag{Name: tag} + tags = append(tags, &t) + } + + return tags +} + func generalError(msg string, err error) error { var diags tfdiags.Diagnostics diff --git a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go index 5e1531330..6e1bd40d9 100644 --- a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go @@ -12,6 +12,7 @@ import ( expect "github.com/Netflix/go-expect" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/internal/e2e" + tfversion "github.com/hashicorp/terraform/version" ) func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { @@ -360,6 +361,108 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) { } }, }, + "migrating multiple workspaces to cloud using tags strategy; existing workspaces": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + tfBlock := terraformConfigLocalBackend() + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init"}, + expectedCmdOutput: `Successfully configured the backend "local"!`, + }, + { + command: []string{"apply", "-auto-approve"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "new", "identity"}, + expectedCmdOutput: `Created and switched to workspace "identity"!`, + }, + { + command: []string{"apply", "-auto-approve"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "new", "billing"}, + expectedCmdOutput: `Created and switched to workspace "billing"!`, + }, + { + command: []string{"apply", "-auto-approve"}, + postInputOutput: []string{`Apply complete!`}, + }, + { + command: []string{"workspace", "select", "default"}, + expectedCmdOutput: `Switched to workspace "default".`, + }, + }, + }, + { + prep: func(t *testing.T, orgName, dir string) { + tag := "app" + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("identity"), + TerraformVersion: tfe.String(tfversion.String()), + }) + _ = createWorkspace(t, orgName, tfe.WorkspaceCreateOptions{ + Name: tfe.String("billing"), + TerraformVersion: tfe.String(tfversion.String()), + }) + tfBlock := terraformConfigCloudBackendTags(orgName, tag) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-migrate-state"}, + expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`, + userInput: []string{"dev", "1", "app-*", "1"}, + postInputOutput: []string{ + `Would you like to rename your workspaces?`, + "What pattern would you like to add to all your workspaces?", + "The currently selected workspace (default) does not exist.", + "Terraform Cloud has been successfully initialized!"}, + }, + { + command: []string{"workspace", "select", "app-dev"}, + expectedCmdOutput: `Switched to workspace "app-dev".`, + }, + { + command: []string{"workspace", "select", "app-billing"}, + expectedCmdOutput: `Switched to workspace "app-billing".`, + }, + { + command: []string{"workspace", "select", "app-identity"}, + expectedCmdOutput: `Switched to workspace "app-identity".`, + }, + }, + }, + }, + validations: func(t *testing.T, orgName string) { + wsList, err := tfeClient.Workspaces.List(ctx, orgName, tfe.WorkspaceListOptions{ + Tags: tfe.String("app"), + }) + if err != nil { + t.Fatal(err) + } + if len(wsList.Items) != 3 { + t.Fatalf("Expected the number of workspaecs to be 3, but got %d", len(wsList.Items)) + } + expectedWorkspaceNames := []string{"app-billing", "app-dev", "app-identity"} + for _, ws := range wsList.Items { + hasName := false + for _, expectedNames := range expectedWorkspaceNames { + if expectedNames == ws.Name { + hasName = true + } + } + if !hasName { + t.Fatalf("Worksapce %s is not in the expected list of workspaces", ws.Name) + } + } + }, + }, } for name, tc := range cases { diff --git a/internal/cloud/tfe_client_mock.go b/internal/cloud/tfe_client_mock.go index ecc3b9add..929e39b89 100644 --- a/internal/cloud/tfe_client_mock.go +++ b/internal/cloud/tfe_client_mock.go @@ -1391,7 +1391,7 @@ func (m *MockWorkspaces) Tags(ctx context.Context, workspaceID string, options t } func (m *MockWorkspaces) AddTags(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagsOptions) error { - panic("not implemented") + return nil } func (m *MockWorkspaces) RemoveTags(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveTagsOptions) error {