|
| 1 | +package exec |
| 2 | + |
| 3 | +import ( |
| 4 | + "os" |
| 5 | + "path/filepath" |
| 6 | + "testing" |
| 7 | + |
| 8 | + log "github.com/cloudposse/atmos/pkg/logger" |
| 9 | + "github.com/stretchr/testify/assert" |
| 10 | + "github.com/stretchr/testify/require" |
| 11 | + |
| 12 | + cfg "github.com/cloudposse/atmos/pkg/config" |
| 13 | + "github.com/cloudposse/atmos/pkg/schema" |
| 14 | + u "github.com/cloudposse/atmos/pkg/utils" |
| 15 | +) |
| 16 | + |
| 17 | +// TestYamlFuncTerraformStateWorkspacesDisabled tests that the !terraform.state YAML function |
| 18 | +// works correctly when Terraform workspaces are disabled (workspaces_enabled: false). |
| 19 | +// |
| 20 | +// When workspaces are disabled: |
| 21 | +// - The workspace name is "default" |
| 22 | +// - For local backend: state is stored at terraform.tfstate (not terraform.tfstate.d/default/terraform.tfstate) |
| 23 | +// - For S3 backend: state is stored at <key> (not <workspace_key_prefix>/default/<key>) |
| 24 | +// |
| 25 | +// See: https://github.com/cloudposse/atmos/issues/1920 |
| 26 | +func TestYamlFuncTerraformStateWorkspacesDisabled(t *testing.T) { |
| 27 | + err := os.Unsetenv("ATMOS_CLI_CONFIG_PATH") |
| 28 | + if err != nil { |
| 29 | + t.Fatalf("Failed to unset 'ATMOS_CLI_CONFIG_PATH': %v", err) |
| 30 | + } |
| 31 | + |
| 32 | + err = os.Unsetenv("ATMOS_BASE_PATH") |
| 33 | + if err != nil { |
| 34 | + t.Fatalf("Failed to unset 'ATMOS_BASE_PATH': %v", err) |
| 35 | + } |
| 36 | + |
| 37 | + log.SetLevel(log.InfoLevel) |
| 38 | + log.SetOutput(os.Stdout) |
| 39 | + |
| 40 | + stack := "test" |
| 41 | + |
| 42 | + defer func() { |
| 43 | + // Delete the generated files and folders after the test. |
| 44 | + mockComponentPath := filepath.Join("..", "..", "tests", "fixtures", "components", "terraform", "mock") |
| 45 | + // Clean up terraform state files. |
| 46 | + err := os.RemoveAll(filepath.Join(mockComponentPath, ".terraform")) |
| 47 | + assert.NoError(t, err) |
| 48 | + |
| 49 | + err = os.RemoveAll(filepath.Join(mockComponentPath, "terraform.tfstate.d")) |
| 50 | + assert.NoError(t, err) |
| 51 | + |
| 52 | + // When workspaces are disabled, state is stored at terraform.tfstate (not in terraform.tfstate.d/). |
| 53 | + err = os.Remove(filepath.Join(mockComponentPath, "terraform.tfstate")) |
| 54 | + // Ignore error if file doesn't exist. |
| 55 | + if err != nil && !os.IsNotExist(err) { |
| 56 | + assert.NoError(t, err) |
| 57 | + } |
| 58 | + |
| 59 | + err = os.Remove(filepath.Join(mockComponentPath, "terraform.tfstate.backup")) |
| 60 | + // Ignore error if file doesn't exist. |
| 61 | + if err != nil && !os.IsNotExist(err) { |
| 62 | + assert.NoError(t, err) |
| 63 | + } |
| 64 | + }() |
| 65 | + |
| 66 | + // Define the working directory (workspaces-disabled fixture). |
| 67 | + workDir := "../../tests/fixtures/scenarios/atmos-terraform-state-yaml-function-workspaces-disabled" |
| 68 | + t.Chdir(workDir) |
| 69 | + |
| 70 | + // Deploy component-1 first to create terraform state. |
| 71 | + info := schema.ConfigAndStacksInfo{ |
| 72 | + StackFromArg: "", |
| 73 | + Stack: stack, |
| 74 | + StackFile: "", |
| 75 | + ComponentType: "terraform", |
| 76 | + ComponentFromArg: "component-1", |
| 77 | + SubCommand: "deploy", |
| 78 | + ProcessTemplates: true, |
| 79 | + ProcessFunctions: true, |
| 80 | + } |
| 81 | + |
| 82 | + err = ExecuteTerraform(info) |
| 83 | + require.NoError(t, err, "Failed to execute 'ExecuteTerraform' for component-1") |
| 84 | + |
| 85 | + // Initialize CLI config. |
| 86 | + atmosConfig, err := cfg.InitCliConfig(info, true) |
| 87 | + require.NoError(t, err) |
| 88 | + |
| 89 | + // Verify that workspaces are disabled. |
| 90 | + require.NotNil(t, atmosConfig.Components.Terraform.WorkspacesEnabled) |
| 91 | + assert.False(t, *atmosConfig.Components.Terraform.WorkspacesEnabled, |
| 92 | + "Expected workspaces to be disabled in this test fixture") |
| 93 | + |
| 94 | + // Test !terraform.state can read outputs from component-1. |
| 95 | + // When workspaces are disabled, the state should be at terraform.tfstate (not terraform.tfstate.d/default/terraform.tfstate). |
| 96 | + d, err := processTagTerraformState(&atmosConfig, "!terraform.state component-1 foo", stack, nil) |
| 97 | + require.NoError(t, err) |
| 98 | + assert.Equal(t, "component-1-a", d, "Expected to read 'foo' output from component-1 state") |
| 99 | + |
| 100 | + d, err = processTagTerraformState(&atmosConfig, "!terraform.state component-1 bar", stack, nil) |
| 101 | + require.NoError(t, err) |
| 102 | + assert.Equal(t, "component-1-b", d, "Expected to read 'bar' output from component-1 state") |
| 103 | + |
| 104 | + d, err = processTagTerraformState(&atmosConfig, "!terraform.state component-1 test baz", "", nil) |
| 105 | + require.NoError(t, err) |
| 106 | + assert.Equal(t, "component-1-c", d, "Expected to read 'baz' output from component-1 state") |
| 107 | + |
| 108 | + // Verify component-2 can use !terraform.state to reference component-1's outputs. |
| 109 | + res, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{ |
| 110 | + Component: "component-2", |
| 111 | + Stack: stack, |
| 112 | + ProcessTemplates: true, |
| 113 | + ProcessYamlFunctions: true, |
| 114 | + Skip: nil, |
| 115 | + AuthManager: nil, |
| 116 | + }) |
| 117 | + require.NoError(t, err) |
| 118 | + |
| 119 | + y, err := u.ConvertToYAML(res) |
| 120 | + require.NoError(t, err) |
| 121 | + assert.Contains(t, y, "foo: component-1-a", "component-2 should have foo from component-1 state") |
| 122 | + assert.Contains(t, y, "bar: component-1-b", "component-2 should have bar from component-1 state") |
| 123 | + assert.Contains(t, y, "baz: component-1-c", "component-2 should have baz from component-1 state") |
| 124 | +} |
| 125 | + |
| 126 | +// TestWorkspacesDisabledStateLocation verifies that when workspaces are disabled, |
| 127 | +// the terraform state is stored at the correct location (terraform.tfstate, not terraform.tfstate.d/default/terraform.tfstate). |
| 128 | +func TestWorkspacesDisabledStateLocation(t *testing.T) { |
| 129 | + err := os.Unsetenv("ATMOS_CLI_CONFIG_PATH") |
| 130 | + require.NoError(t, err) |
| 131 | + |
| 132 | + err = os.Unsetenv("ATMOS_BASE_PATH") |
| 133 | + require.NoError(t, err) |
| 134 | + |
| 135 | + log.SetLevel(log.InfoLevel) |
| 136 | + log.SetOutput(os.Stdout) |
| 137 | + |
| 138 | + stack := "test" |
| 139 | + |
| 140 | + // Get the absolute path to the mock component before changing directories. |
| 141 | + // The path is relative to the current working directory (internal/exec). |
| 142 | + mockComponentPath, err := filepath.Abs("../../tests/fixtures/components/terraform/mock") |
| 143 | + require.NoError(t, err) |
| 144 | + |
| 145 | + defer func() { |
| 146 | + // Clean up terraform state files. |
| 147 | + err := os.RemoveAll(filepath.Join(mockComponentPath, ".terraform")) |
| 148 | + assert.NoError(t, err) |
| 149 | + |
| 150 | + err = os.RemoveAll(filepath.Join(mockComponentPath, "terraform.tfstate.d")) |
| 151 | + assert.NoError(t, err) |
| 152 | + |
| 153 | + err = os.Remove(filepath.Join(mockComponentPath, "terraform.tfstate")) |
| 154 | + if err != nil && !os.IsNotExist(err) { |
| 155 | + assert.NoError(t, err) |
| 156 | + } |
| 157 | + |
| 158 | + err = os.Remove(filepath.Join(mockComponentPath, "terraform.tfstate.backup")) |
| 159 | + if err != nil && !os.IsNotExist(err) { |
| 160 | + assert.NoError(t, err) |
| 161 | + } |
| 162 | + }() |
| 163 | + |
| 164 | + // Define the working directory (workspaces-disabled fixture). |
| 165 | + workDir := "../../tests/fixtures/scenarios/atmos-terraform-state-yaml-function-workspaces-disabled" |
| 166 | + t.Chdir(workDir) |
| 167 | + |
| 168 | + // Deploy component-1. |
| 169 | + info := schema.ConfigAndStacksInfo{ |
| 170 | + StackFromArg: "", |
| 171 | + Stack: stack, |
| 172 | + StackFile: "", |
| 173 | + ComponentType: "terraform", |
| 174 | + ComponentFromArg: "component-1", |
| 175 | + SubCommand: "deploy", |
| 176 | + ProcessTemplates: true, |
| 177 | + ProcessFunctions: true, |
| 178 | + } |
| 179 | + |
| 180 | + err = ExecuteTerraform(info) |
| 181 | + require.NoError(t, err, "Failed to deploy component-1") |
| 182 | + |
| 183 | + // Verify that the state file is at terraform.tfstate (not terraform.tfstate.d/default/terraform.tfstate). |
| 184 | + stateFilePath := filepath.Join(mockComponentPath, "terraform.tfstate") |
| 185 | + wrongStatePath := filepath.Join(mockComponentPath, "terraform.tfstate.d", "default", "terraform.tfstate") |
| 186 | + |
| 187 | + // State should exist at the correct location. |
| 188 | + _, err = os.Stat(stateFilePath) |
| 189 | + assert.NoError(t, err, "State file should exist at %s when workspaces are disabled", stateFilePath) |
| 190 | + |
| 191 | + // State should NOT exist at the wrong location. |
| 192 | + _, err = os.Stat(wrongStatePath) |
| 193 | + assert.True(t, os.IsNotExist(err), "State file should NOT exist at %s when workspaces are disabled", wrongStatePath) |
| 194 | +} |
0 commit comments