diff --git a/Makefile b/Makefile index 38d977c00..bc12271ec 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -TEST?=$$(go list ./... | grep -v 'vendor') +TEST ?= $$(go list ./... | grep -v 'vendor') SHELL := /bin/bash #GOOS=darwin -GOOS=linux -GOARCH=amd64 +#GOOS=linux +#GOARCH=amd64 VERSION=test # List of targets the `readme` target should call before generating the readme @@ -18,7 +18,7 @@ get: go get build: get - env GOOS=${GOOS} GOARCH=${GOARCH} go build -o build/atmos -v -ldflags "-X 'github.com/cloudposse/atmos/cmd.Version=${VERSION}'" + env $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) go build -o build/atmos -v -ldflags "-X 'github.com/cloudposse/atmos/cmd.Version=${VERSION}'" version: build chmod +x ./build/atmos diff --git a/internal/exec/shell_utils.go b/internal/exec/shell_utils.go index f93675be9..9ddb12db7 100644 --- a/internal/exec/shell_utils.go +++ b/internal/exec/shell_utils.go @@ -45,8 +45,9 @@ func ExecuteShellCommand( } else if redirectStdError == "" { cmd.Stderr = os.Stderr } else { - f, err := os.OpenFile(redirectStdError, os.O_RDWR|os.O_CREATE, 0644) + f, err := os.OpenFile(redirectStdError, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { + u.LogWarning(cliConfig, err.Error()) return err } diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index fe2c87688..a08b9374b 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -3,6 +3,7 @@ package exec import ( "fmt" "os" + osexec "os/exec" "path" "strings" @@ -318,11 +319,6 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { u.LogDebug(cliConfig, fmt.Sprintf("Working dir: %s", workingDir)) - // Set terraform workspace via ENV var - if !(info.SubCommand == "workspace" && info.SubCommand2 != "") { - info.ComponentEnvList = append(info.ComponentEnvList, fmt.Sprintf("TF_WORKSPACE=%s", info.TerraformWorkspace)) - } - // Print ENV vars if they are found in the component's stack config if len(info.ComponentEnvList) > 0 { u.LogDebug(cliConfig, "\nUsing ENV vars:") @@ -374,6 +370,46 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { allArgsAndFlags = append(allArgsAndFlags, info.AdditionalArgsAndFlags...) + // Run `terraform workspace` before executing other terraform commands + if info.SubCommand != "init" && !(info.SubCommand == "workspace" && info.SubCommand2 != "") { + workspaceSelectRedirectStdErr := "/dev/stdout" + + // If `--redirect-stderr` flag is not passed, always redirect `stderr` to `stdout` for `terraform workspace select` command + if info.RedirectStdErr != "" { + workspaceSelectRedirectStdErr = info.RedirectStdErr + } + + err = ExecuteShellCommand( + cliConfig, + info.Command, + []string{"workspace", "select", info.TerraformWorkspace}, + componentPath, + info.ComponentEnvList, + info.DryRun, + workspaceSelectRedirectStdErr, + ) + if err != nil { + var osErr *osexec.ExitError + ok := errors.As(err, &osErr) + if !ok || osErr.ExitCode() != 1 { + // err is not a non-zero exit code or err is not exit code 1, which we are expecting + return err + } + err = ExecuteShellCommand( + cliConfig, + info.Command, + []string{"workspace", "new", info.TerraformWorkspace}, + componentPath, + info.ComponentEnvList, + info.DryRun, + info.RedirectStdErr, + ) + if err != nil { + return err + } + } + } + // Check if the terraform command requires a user interaction, // but it's running in a scripted environment (where a `tty` is not attached or `stdin` is not attached) if os.Stdin == nil && !u.SliceContainsString(info.AdditionalArgsAndFlags, autoApproveFlag) { diff --git a/website/docs/core-concepts/components/terraform-workspaces.mdx b/website/docs/core-concepts/components/terraform-workspaces.mdx index 12e6942b2..258b3e1f1 100644 --- a/website/docs/core-concepts/components/terraform-workspaces.mdx +++ b/website/docs/core-concepts/components/terraform-workspaces.mdx @@ -31,12 +31,11 @@ For example, you might use separate workspaces for blue-green deployments or can To work with workspaces in Terraform, you can use commands like `terraform workspace new`, `terraform workspace select`, and `terraform workspace delete` to create, switch between, and delete workspaces respectively. -Additionally, the ENV variable [`TF_WORKSPACE`](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_workspace) -can be used to create and select Terraform workspaces. +Atmos automatically manages Terraform workspaces for you when you provision components in a stack. ## Terraform Workspaces in Atmos -Atmos uses and automatically calculates Terraform workspaces to manage top-level stacks. By default, Atmos uses the stack +Atmos automatically calculates Terraform workspace names and uses workspaces to manage top-level stacks. By default, Atmos uses the stack name as the Terraform workspace when provisioning components in the stack. For example, consider the following manifest for the component `vpc` in the stack `ue2-dev`: @@ -72,8 +71,10 @@ When you provision the `vpc` component in the stack `ue2-dev` by executing the f
-Atmos sets the [`TF_WORKSPACE`](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_workspace) -ENV variable to the name of the stack `ue2-dev`, and Terraform then selects this workspace or creates it if it does not exist. +Atmos computes the workspace name to be `ue2-dev`. Any Atmos Terraform command other than `init`, using this stack, +will cause Atmos to select this workspace, creating it if needed. (This leaves the workspace selected as a side effect +for subsequent Terraform commands run outside of Atmos. Atmos version 1.55 took away this side effect, but it was +restored in version 1.69.) The exception to the default rule (using the stack name as Terraform workspace) is when we provision more than one instance of the same Terraform component (with the same or different settings) into the same stack by defining multiple @@ -133,9 +134,9 @@ When you provision the components by executing the commands:
-Atmos sets the [`TF_WORKSPACE`](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_workspace) -environment variable to `ue2-dev-vpc-1` and `ue2-dev-vpc-2` respectively, and Terraform selects (or creates) different workspaces -for the components. This is done because the same Terraform component `vpc` is used as the workspace prefix +Atmos computes the workspace names as `ue2-dev-vpc-1` and `ue2-dev-vpc-2` respectively, +and selects the appropriate workspace for each component (again, creating it if needed). +This is done because the same Terraform component `vpc` is used as the workspace prefix (in case of [AWS S3 backend](https://developer.hashicorp.com/terraform/language/settings/backends/s3), folder in the S3 state bucket), and it's necessary to have different subfolders (`ue2-dev-vpc-1` and `ue2-dev-vpc-2` instead of just `ue2-dev`) to store the Terraform state files.