Skip to content

Commit 24fe365

Browse files
authored
Merge branch 'main' into feature/dev-3755-custom-component-types
2 parents cc68e36 + 2cd3971 commit 24fe365

File tree

104 files changed

+12393
-1418
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+12393
-1418
lines changed

.golangci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,16 @@ linters:
316316
- forbidigo
317317
path: _test\.go$
318318
text: "viper\\.BindEnv|viper\\.BindPFlag"
319+
# These files are temporarily over the 500-line limit due to terraform caching refactoring.
320+
# TODO: Refactor these files to reduce their size.
321+
- linters:
322+
- revive
323+
path: internal/exec/terraform\.go$
324+
text: "file-length-limit"
325+
- linters:
326+
- revive
327+
path: internal/exec/terraform_clean\.go$
328+
text: "file-length-limit"
319329
paths:
320330
- experiments/.*
321331
- third_party$

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ This repository uses git worktrees for parallel development. When working in a w
1919

2020
**For Task agents:** When searching for files, always use the current working directory (`.`) or relative paths. Never construct absolute paths that might escape the worktree.
2121

22+
## Concurrent Sessions (MANDATORY)
23+
24+
Multiple Claude sessions may be working on the same branch or worktree simultaneously. To avoid destroying other sessions' work:
25+
26+
- **NEVER delete, reset, or discard files you didn't create** - Other sessions may have created them
27+
- **NEVER run `git reset`, `git checkout --`, or `git clean`** without explicit user approval
28+
- **ALWAYS ask the user before removing untracked files** - They may be work-in-progress from another session
29+
- **When you see unfamiliar files**, assume another session created them - ask the user what to do
30+
- **If pre-commit hooks fail due to files you didn't touch**, ask the user how to proceed rather than trying to fix or remove them
31+
32+
**Why this matters:** The user may have multiple Claude sessions working in parallel on different aspects of a feature. Deleting “unknown” files destroys that work.
33+
2234
## Essential Commands
2335

2436
```bash

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"github.com/cloudposse/atmos/internal/tui/templates"
2929
"github.com/cloudposse/atmos/internal/tui/templates/term"
3030
cfg "github.com/cloudposse/atmos/pkg/config"
31+
// Import adapters to register them with the config package.
32+
_ "github.com/cloudposse/atmos/pkg/config/adapters"
3133
"github.com/cloudposse/atmos/pkg/data"
3234
"github.com/cloudposse/atmos/pkg/filesystem"
3335
"github.com/cloudposse/atmos/pkg/flags"

cmd/terraform/clean.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Common use cases:
5252
everything := v.GetBool("everything")
5353
skipLockFile := v.GetBool("skip-lock-file")
5454
dryRun := v.GetBool("dry-run")
55+
cache := v.GetBool("cache")
5556

5657
// Prompt for component/stack if neither is provided.
5758
if component == "" && stack == "" {
@@ -78,6 +79,7 @@ Common use cases:
7879
Everything: everything,
7980
SkipLockFile: skipLockFile,
8081
DryRun: dryRun,
82+
Cache: cache,
8183
}
8284
return e.ExecuteClean(opts, &atmosConfig)
8385
},
@@ -89,9 +91,11 @@ func init() {
8991
flags.WithBoolFlag("everything", "", false, "If set atmos will also delete the Terraform state files and directories for the component"),
9092
flags.WithBoolFlag("force", "f", false, "Forcefully delete Terraform state files and directories without interaction"),
9193
flags.WithBoolFlag("skip-lock-file", "", false, "Skip deleting the `.terraform.lock.hcl` file"),
94+
flags.WithBoolFlag("cache", "", false, "Clean Terraform plugin cache directory"),
9295
flags.WithEnvVars("everything", "ATMOS_TERRAFORM_CLEAN_EVERYTHING"),
9396
flags.WithEnvVars("force", "ATMOS_TERRAFORM_CLEAN_FORCE"),
9497
flags.WithEnvVars("skip-lock-file", "ATMOS_TERRAFORM_CLEAN_SKIP_LOCK_FILE"),
98+
flags.WithEnvVars("cache", "ATMOS_TERRAFORM_CLEAN_CACHE"),
9599
)
96100

97101
// Register flags with the command as persistent flags.

cmd/terraform/clean_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package terraform
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/viper"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestCleanCommandSetup verifies that the clean command is properly configured.
12+
func TestCleanCommandSetup(t *testing.T) {
13+
// Verify command is registered.
14+
require.NotNil(t, cleanCmd)
15+
16+
// Verify it's attached to terraformCmd.
17+
found := false
18+
for _, cmd := range terraformCmd.Commands() {
19+
if cmd.Name() == "clean" {
20+
found = true
21+
break
22+
}
23+
}
24+
assert.True(t, found, "clean should be registered as a subcommand of terraformCmd")
25+
26+
// Verify command short and long descriptions.
27+
assert.Contains(t, cleanCmd.Short, "Clean")
28+
assert.Contains(t, cleanCmd.Long, "Terraform")
29+
}
30+
31+
// TestCleanParserSetup verifies that the clean parser is properly configured.
32+
func TestCleanParserSetup(t *testing.T) {
33+
require.NotNil(t, cleanParser, "cleanParser should be initialized")
34+
35+
// Verify the parser has the clean-specific flags.
36+
registry := cleanParser.Registry()
37+
38+
expectedFlags := []string{
39+
"everything",
40+
"force",
41+
"skip-lock-file",
42+
"cache",
43+
}
44+
45+
for _, flagName := range expectedFlags {
46+
assert.True(t, registry.Has(flagName), "cleanParser should have %s flag registered", flagName)
47+
}
48+
}
49+
50+
// TestCleanFlagSetup verifies that clean command has correct flags registered.
51+
func TestCleanFlagSetup(t *testing.T) {
52+
// Verify clean-specific flags are registered on the command.
53+
cleanFlags := []string{
54+
"everything",
55+
"force",
56+
"skip-lock-file",
57+
"cache",
58+
}
59+
60+
for _, flagName := range cleanFlags {
61+
flag := cleanCmd.Flags().Lookup(flagName)
62+
assert.NotNil(t, flag, "%s flag should be registered on clean command", flagName)
63+
}
64+
}
65+
66+
// TestCleanFlagDefaults verifies that clean command flags have correct default values.
67+
func TestCleanFlagDefaults(t *testing.T) {
68+
v := viper.New()
69+
70+
// Bind parser to fresh viper instance.
71+
err := cleanParser.BindToViper(v)
72+
require.NoError(t, err)
73+
74+
// Verify default values.
75+
assert.False(t, v.GetBool("everything"), "everything should default to false")
76+
assert.False(t, v.GetBool("force"), "force should default to false")
77+
assert.False(t, v.GetBool("skip-lock-file"), "skip-lock-file should default to false")
78+
assert.False(t, v.GetBool("cache"), "cache should default to false")
79+
}
80+
81+
// TestCleanFlagEnvVars verifies that clean command flags have environment variable bindings.
82+
func TestCleanFlagEnvVars(t *testing.T) {
83+
registry := cleanParser.Registry()
84+
85+
// Expected env var bindings.
86+
expectedEnvVars := map[string]string{
87+
"everything": "ATMOS_TERRAFORM_CLEAN_EVERYTHING",
88+
"force": "ATMOS_TERRAFORM_CLEAN_FORCE",
89+
"skip-lock-file": "ATMOS_TERRAFORM_CLEAN_SKIP_LOCK_FILE",
90+
"cache": "ATMOS_TERRAFORM_CLEAN_CACHE",
91+
}
92+
93+
for flagName, expectedEnvVar := range expectedEnvVars {
94+
require.True(t, registry.Has(flagName), "cleanParser should have %s flag registered", flagName)
95+
flag := registry.Get(flagName)
96+
require.NotNil(t, flag, "cleanParser should have info for %s flag", flagName)
97+
envVars := flag.GetEnvVars()
98+
assert.Contains(t, envVars, expectedEnvVar, "%s should be bound to %s", flagName, expectedEnvVar)
99+
}
100+
}
101+
102+
// TestCleanCommandArgs verifies that clean command accepts the correct number of arguments.
103+
func TestCleanCommandArgs(t *testing.T) {
104+
// The command should accept 0 or 1 argument (component name is optional).
105+
require.NotNil(t, cleanCmd.Args)
106+
107+
// Verify with no args.
108+
err := cleanCmd.Args(cleanCmd, []string{})
109+
assert.NoError(t, err, "clean command should accept 0 arguments")
110+
111+
// Verify with one arg.
112+
err = cleanCmd.Args(cleanCmd, []string{"my-component"})
113+
assert.NoError(t, err, "clean command should accept 1 argument")
114+
115+
// Verify with two args (should fail).
116+
err = cleanCmd.Args(cleanCmd, []string{"arg1", "arg2"})
117+
assert.Error(t, err, "clean command should reject more than 1 argument")
118+
}

cmd/terraform/output.go

Lines changed: 170 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,195 @@
11
package terraform
22

33
import (
4+
"slices"
5+
"strings"
6+
47
"github.com/spf13/cobra"
8+
"github.com/spf13/viper"
59

610
"github.com/cloudposse/atmos/cmd/internal"
11+
errUtils "github.com/cloudposse/atmos/errors"
12+
exec "github.com/cloudposse/atmos/internal/exec"
13+
cfg "github.com/cloudposse/atmos/pkg/config"
14+
"github.com/cloudposse/atmos/pkg/data"
15+
"github.com/cloudposse/atmos/pkg/flags"
16+
"github.com/cloudposse/atmos/pkg/flags/compat"
17+
"github.com/cloudposse/atmos/pkg/perf"
18+
"github.com/cloudposse/atmos/pkg/schema"
19+
tfoutput "github.com/cloudposse/atmos/pkg/terraform/output"
720
)
821

22+
// outputParser handles flag parsing for output command.
23+
var outputParser *flags.StandardParser
24+
925
// outputCmd represents the terraform output command.
1026
var outputCmd = &cobra.Command{
1127
Use: "output",
1228
Short: "Show output values from your root module",
13-
Long: `Read an output variable from the state file.
29+
Long: `Read output variables from the state file.
30+
31+
When --format is specified, retrieves all outputs and formats them in the specified format.
32+
Without --format, passes through to native terraform/tofu output command.
1433
1534
For complete Terraform/OpenTofu documentation, see:
1635
https://developer.hashicorp.com/terraform/cli/commands/output
1736
https://opentofu.org/docs/cli/commands/output`,
1837
RunE: func(cmd *cobra.Command, args []string) error {
19-
return terraformRun(terraformCmd, cmd, args)
38+
v := viper.GetViper()
39+
if err := terraformParser.BindFlagsToViper(cmd, v); err != nil {
40+
return err
41+
}
42+
if err := outputParser.BindFlagsToViper(cmd, v); err != nil {
43+
return err
44+
}
45+
format := v.GetString("format")
46+
if format == "" {
47+
return terraformRun(terraformCmd, cmd, args)
48+
}
49+
return outputRunWithFormat(cmd, args, format)
2050
},
2151
}
2252

53+
// outputRunWithFormat executes terraform output with atmos formatting.
54+
func outputRunWithFormat(cmd *cobra.Command, args []string, format string) error {
55+
defer perf.Track(nil, "terraform.outputRunWithFormat")()
56+
57+
if err := validateOutputFormat(format); err != nil {
58+
return err
59+
}
60+
info, atmosConfig, err := prepareOutputContext(cmd, args)
61+
if err != nil {
62+
return err
63+
}
64+
return executeOutputWithFormat(atmosConfig, info, format)
65+
}
66+
67+
// validateOutputFormat checks if the format is supported.
68+
func validateOutputFormat(format string) error {
69+
if !slices.Contains(tfoutput.SupportedFormats, format) {
70+
return errUtils.Build(errUtils.ErrInvalidArgumentError).
71+
WithExplanationf("Invalid --format value %q.", format).
72+
WithHintf("Supported formats: %s.", strings.Join(tfoutput.SupportedFormats, ", ")).
73+
Err()
74+
}
75+
return nil
76+
}
77+
78+
// prepareOutputContext validates config and prepares component info.
79+
func prepareOutputContext(cmd *cobra.Command, args []string) (*schema.ConfigAndStacksInfo, *schema.AtmosConfiguration, error) {
80+
if err := internal.ValidateAtmosConfig(); err != nil {
81+
return nil, nil, err
82+
}
83+
separatedArgs := compat.GetSeparated()
84+
argsWithSubCommand := append([]string{"output"}, args...)
85+
info, err := exec.ProcessCommandLineArgs(cfg.TerraformComponentType, terraformCmd, argsWithSubCommand, separatedArgs)
86+
if err != nil {
87+
return nil, nil, err
88+
}
89+
if err := resolveAndPromptForArgs(&info, cmd); err != nil {
90+
return nil, nil, err
91+
}
92+
v := viper.GetViper()
93+
globalFlags := flags.ParseGlobalFlags(cmd, v)
94+
configAndStacksInfo := schema.ConfigAndStacksInfo{
95+
AtmosBasePath: globalFlags.BasePath,
96+
AtmosConfigFilesFromArg: globalFlags.Config,
97+
AtmosConfigDirsFromArg: globalFlags.ConfigPath,
98+
ProfilesFromArg: globalFlags.Profile,
99+
ComponentFromArg: info.ComponentFromArg,
100+
Stack: info.Stack,
101+
}
102+
atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true)
103+
if err != nil {
104+
return nil, nil, errUtils.Build(errUtils.ErrInitializeCLIConfig).WithCause(err).Err()
105+
}
106+
return &info, &atmosConfig, nil
107+
}
108+
109+
// executeOutputWithFormat retrieves and formats terraform outputs.
110+
func executeOutputWithFormat(atmosConfig *schema.AtmosConfiguration, info *schema.ConfigAndStacksInfo, format string) error {
111+
v := viper.GetViper()
112+
skipInit := v.GetBool("skip-init")
113+
outputFile := v.GetString("output-file")
114+
uppercase := v.GetBool("uppercase")
115+
flatten := v.GetBool("flatten")
116+
117+
outputs, err := tfoutput.GetComponentOutputs(atmosConfig, info.ComponentFromArg, info.Stack, skipInit)
118+
if err != nil {
119+
return errUtils.Build(errUtils.ErrTerraformOutputFailed).
120+
WithCause(err).
121+
WithExplanationf("Failed to get terraform outputs for component %q in stack %q.", info.ComponentFromArg, info.Stack).
122+
Err()
123+
}
124+
125+
// Build format options.
126+
opts := tfoutput.FormatOptions{
127+
Uppercase: uppercase,
128+
Flatten: flatten,
129+
}
130+
131+
// Check if a specific output name was requested (in AdditionalArgsAndFlags).
132+
outputName := extractOutputName(info.AdditionalArgsAndFlags)
133+
var formatted string
134+
if outputName != "" {
135+
formatted, err = formatSingleOutput(outputs, outputName, format, opts)
136+
} else {
137+
formatted, err = tfoutput.FormatOutputsWithOptions(outputs, tfoutput.Format(format), opts)
138+
}
139+
if err != nil {
140+
return err
141+
}
142+
143+
if outputFile != "" {
144+
return tfoutput.WriteToFile(outputFile, formatted)
145+
}
146+
return data.Write(formatted)
147+
}
148+
149+
// extractOutputName extracts the output name from additional args.
150+
// Output name is a positional arg that doesn't start with "-".
151+
func extractOutputName(args []string) string {
152+
for _, arg := range args {
153+
if !strings.HasPrefix(arg, "-") {
154+
return arg
155+
}
156+
}
157+
return ""
158+
}
159+
160+
// formatSingleOutput formats a single output value.
161+
func formatSingleOutput(outputs map[string]any, outputName, format string, opts tfoutput.FormatOptions) (string, error) {
162+
value, exists := outputs[outputName]
163+
if !exists {
164+
return "", errUtils.Build(errUtils.ErrTerraformOutputFailed).
165+
WithExplanationf("Output %q not found.", outputName).
166+
WithHint("Use 'atmos terraform output <component> -s <stack>' without an output name to see all available outputs.").
167+
Err()
168+
}
169+
return tfoutput.FormatSingleValueWithOptions(outputName, value, tfoutput.Format(format), opts)
170+
}
171+
23172
func init() {
24-
// Register completions for outputCmd.
173+
outputParser = flags.NewStandardParser(
174+
flags.WithStringFlag("format", "f", "", "Output format: json, yaml, hcl, env, dotenv, bash, csv, tsv"),
175+
flags.WithStringFlag("output-file", "o", "", "Write output to file instead of stdout"),
176+
flags.WithBoolFlag("uppercase", "u", false, "Convert keys to uppercase (useful for env vars)"),
177+
flags.WithBoolFlag("flatten", "", false, "Flatten nested maps into key_subkey format"),
178+
flags.WithEnvVars("format", "ATMOS_TERRAFORM_OUTPUT_FORMAT"),
179+
flags.WithEnvVars("output-file", "ATMOS_TERRAFORM_OUTPUT_FILE"),
180+
flags.WithEnvVars("uppercase", "ATMOS_TERRAFORM_OUTPUT_UPPERCASE"),
181+
flags.WithEnvVars("flatten", "ATMOS_TERRAFORM_OUTPUT_FLATTEN"),
182+
)
183+
outputParser.RegisterFlags(outputCmd)
184+
if err := outputParser.BindToViper(viper.GetViper()); err != nil {
185+
panic(err)
186+
}
187+
if err := outputCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
188+
return tfoutput.SupportedFormats, cobra.ShellCompDirectiveNoFileComp
189+
}); err != nil {
190+
_ = err
191+
}
25192
RegisterTerraformCompletions(outputCmd)
26-
27-
// Register compat flags for this subcommand.
28193
internal.RegisterCommandCompatFlags("terraform", "output", OutputCompatFlags())
29-
30-
// Attach to parent terraform command.
31194
terraformCmd.AddCommand(outputCmd)
32195
}

0 commit comments

Comments
 (0)