Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0dd4c3e
Add generate section inheritance and auto-generation support
osterman Dec 16, 2025
4792e63
Use pretty-printed YAML output for file generation
osterman Dec 16, 2025
f8570b0
Address CodeRabbit review feedback
osterman Dec 16, 2025
63082db
refactor: Migrate terraform clean/generate to pkg with adapter pattern
osterman Dec 17, 2025
e7d9dfe
refactor: Complete migration of terraform clean to pkg/terraform/clean
osterman Dec 17, 2025
4987ed9
test: Expand test coverage for terraform clean and generate packages
osterman Dec 18, 2025
395ed6a
fix: Use filepath.Separator for cross-platform test compatibility
osterman Dec 18, 2025
b9e51af
test: Add tests for terraform generate files command
osterman Dec 19, 2025
f0b36b0
feat: Add go:embed usage examples for terraform clean and generate files
osterman Dec 19, 2025
3ba205e
Address CodeRabbit review feedback
osterman Dec 19, 2025
8385a28
docs: Add stack configuration documentation for generate section
osterman Dec 19, 2025
b42968b
docs: Move ActionCard before Arguments section per convention
osterman Dec 19, 2025
83088eb
docs: Add docs and changelog links to generate files roadmap entry
osterman Dec 27, 2025
2fa45de
fix: Address CodeRabbit review feedback
osterman Dec 27, 2025
9e28ce7
fix: address CodeRabbit review comments
osterman Dec 29, 2025
2cc2182
Merge branch 'main' into osterman/generate-section-prd
aknysh Jan 4, 2026
0c5c288
fix: improve grammar in error messages and add Cache field to Options
osterman Jan 4, 2026
afcf15b
fix: correct broken link path for generate files command
osterman Jan 4, 2026
2e02feb
address comments, add tests
aknysh Jan 4, 2026
211e4f9
Merge remote-tracking branch 'origin/osterman/generate-section-prd' i…
aknysh Jan 4, 2026
fbec904
Merge remote-tracking branch 'origin/main' into osterman/generate-sec…
aknysh Jan 4, 2026
732b526
address comments, improve/add docs
aknysh Jan 5, 2026
894fa5c
add test fixtures and integration tests
aknysh Jan 5, 2026
7f886ad
fix 'generate' inheritance, update tests, PRD and blog post
aknysh Jan 5, 2026
d330920
add 'generate' to Atmos JSON schema
aknysh Jan 5, 2026
37137b5
address comments, add tests
aknysh Jan 5, 2026
b5f31f5
address comments, add tests
aknysh Jan 5, 2026
d240bfe
address comments, fix tests
aknysh Jan 5, 2026
3e7942f
fix `--stacks` flag, address comments, add tests
aknysh Jan 5, 2026
ad99e3b
address comments, fix tests
aknysh Jan 5, 2026
2f1ac06
address comments, add tests
aknysh Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions cmd/markdown/atmos_terraform_clean_usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
- Clean all components across all stacks

```shell
$ atmos terraform clean
```

- Clean a specific component

```shell
$ atmos terraform clean vpc
```

- Clean a component in a specific stack

```shell
$ atmos terraform clean vpc -s prod-ue2
```

- Force clean without confirmation

```shell
$ atmos terraform clean vpc -s prod-ue2 --force
```

- Clean everything including state files

```shell
$ atmos terraform clean vpc -s prod-ue2 --everything
```

- Dry run to see what would be deleted

```shell
$ atmos terraform clean --dry-run
```

- Skip deleting the lock file

```shell
$ atmos terraform clean vpc -s prod-ue2 --skip-lock-file
```
35 changes: 35 additions & 0 deletions cmd/markdown/atmos_terraform_generate_files_usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
- Generate files for a single component

```shell
atmos terraform generate files vpc -s prod-ue2
```

- Generate files for all components

```shell
atmos terraform generate files --all
```

- Dry run to see what would be generated

```shell
atmos terraform generate files vpc -s prod-ue2 --dry-run
```

- Delete generated files

```shell
atmos terraform generate files --clean --all
```

- Filter by specific stacks

```shell
atmos terraform generate files --all --stacks "prod-*"
```

- Filter by specific components

```shell
atmos terraform generate files --all --components vpc,rds
```
17 changes: 15 additions & 2 deletions cmd/terraform/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/flags"
"github.com/cloudposse/atmos/pkg/schema"
tfclean "github.com/cloudposse/atmos/pkg/terraform/clean"
)

// cleanParser handles flag parsing for clean command.
Expand Down Expand Up @@ -72,7 +73,7 @@ Common use cases:
return err
}

opts := &e.CleanOptions{
opts := &tfclean.Options{
Component: component,
Stack: stack,
Force: force,
Expand All @@ -81,7 +82,19 @@ Common use cases:
DryRun: dryRun,
Cache: cache,
}
return e.ExecuteClean(opts, &atmosConfig)

// Create adapter with exec functions and execute clean.
adapter := tfclean.NewExecAdapter(
e.ProcessStacksForClean,
e.ExecuteDescribeStacksForClean,
e.GetGenerateFilenamesForComponent,
e.CollectComponentsDirectoryObjectsForClean,
e.ConstructTerraformComponentVarfileNameForClean,
e.ConstructTerraformComponentPlanfileNameForClean,
e.GetAllStacksComponentsPathsForClean,
)
service := tfclean.NewService(adapter)
return service.Execute(opts, &atmosConfig)
},
}

Expand Down
75 changes: 75 additions & 0 deletions cmd/terraform/clean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,78 @@ func TestCleanCommandArgs(t *testing.T) {
err = cleanCmd.Args(cleanCmd, []string{"arg1", "arg2"})
assert.Error(t, err, "clean command should reject more than 1 argument")
}

// TestCleanCommandDescription verifies the clean command has proper descriptions.
func TestCleanCommandDescription(t *testing.T) {
t.Run("short description is meaningful", func(t *testing.T) {
assert.NotEmpty(t, cleanCmd.Short)
assert.Contains(t, cleanCmd.Short, "Clean")
})

t.Run("long description explains use cases", func(t *testing.T) {
assert.Contains(t, cleanCmd.Long, "state")
assert.Contains(t, cleanCmd.Long, "artifacts")
})
}

// TestCleanCommandFlagTypes verifies that clean flags have correct types.
func TestCleanCommandFlagTypes(t *testing.T) {
// All clean-specific flags are bool flags.
boolFlags := []string{"everything", "force", "skip-lock-file", "cache"}

for _, flagName := range boolFlags {
flag := cleanCmd.Flags().Lookup(flagName)
require.NotNil(t, flag, "%s flag should exist", flagName)
assert.Equal(t, "bool", flag.Value.Type(), "%s should be a bool flag", flagName)
}
}

// TestCleanCommandFlagShorthands verifies that clean flags have correct shorthands.
func TestCleanCommandFlagShorthands(t *testing.T) {
tests := []struct {
flag string
shorthand string
}{
{"force", "f"},
{"everything", ""}, // No shorthand.
{"skip-lock-file", ""}, // No shorthand.
{"cache", ""}, // No shorthand.
}

for _, tt := range tests {
t.Run(tt.flag+" shorthand", func(t *testing.T) {
flag := cleanCmd.Flags().Lookup(tt.flag)
require.NotNil(t, flag)
assert.Equal(t, tt.shorthand, flag.Shorthand)
})
}
}

// TestCleanParserRegistry verifies the clean parser registry is correctly set up.
func TestCleanParserRegistry(t *testing.T) {
registry := cleanParser.Registry()
require.NotNil(t, registry)

// Verify all expected flags exist.
expectedFlags := []string{"everything", "force", "skip-lock-file", "cache"}

for _, flagName := range expectedFlags {
t.Run(flagName+" in registry", func(t *testing.T) {
assert.True(t, registry.Has(flagName))
flagInfo := registry.Get(flagName)
assert.NotNil(t, flagInfo)
})
}
}

// TestCleanCommandUsage verifies the command usage string.
func TestCleanCommandUsage(t *testing.T) {
assert.Equal(t, "clean <component>", cleanCmd.Use)
}

// TestCleanCommandIsSubcommand verifies clean is properly attached to terraform.
func TestCleanCommandIsSubcommand(t *testing.T) {
parent := cleanCmd.Parent()
assert.NotNil(t, parent)
assert.Equal(t, "terraform", parent.Name())
}
134 changes: 134 additions & 0 deletions cmd/terraform/generate/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package generate

import (
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/viper"

errUtils "github.com/cloudposse/atmos/errors"
e "github.com/cloudposse/atmos/internal/exec"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/flags"
"github.com/cloudposse/atmos/pkg/schema"
tfgenerate "github.com/cloudposse/atmos/pkg/terraform/generate"
)

// filesParser handles flag parsing for files command.
var filesParser *flags.StandardParser

// filesCmd generates files for terraform components from the generate section.
var filesCmd = &cobra.Command{
Use: "files [component]",
Short: "Generate files for Terraform components from the generate section",
Long: `Generate additional configuration files for Terraform components based on
the generate section in stack configuration.

When called with a component argument, generates files for that component.
When called with --all, generates files for all components across stacks.

The generate section in stack configuration supports:
- Map values: serialized based on file extension (.json, .yaml, .hcl, .tf)
- String values: written as literal templates with Go template support`,
Args: cobra.MaximumNArgs(1),
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
RunE: func(cmd *cobra.Command, args []string) error {
// Use Viper to respect precedence (flag > env > config > default).
v := viper.GetViper()

// Bind files-specific flags to Viper.
if err := filesParser.BindFlagsToViper(cmd, v); err != nil {
return err
}

// Get flag values from Viper.
stack := v.GetString("stack")
all := v.GetBool("all")
stacksCsv := v.GetString("stacks")
componentsCsv := v.GetString("components")
dryRun := v.GetBool("dry-run")
clean := v.GetBool("clean")

// Validate: component requires stack, --all excludes component.
if len(args) > 0 && all {
return fmt.Errorf("%w: cannot specify both component and --all", errUtils.ErrInvalidFlag)
}
if len(args) > 0 && stack == "" {
return fmt.Errorf("%w: --stack is required when specifying a component", errUtils.ErrInvalidFlag)
}
if len(args) == 0 && !all {
return fmt.Errorf("%w: either specify a component or use --all", errUtils.ErrInvalidFlag)
}

// Parse CSV values with whitespace trimming.
var stacks []string
if stacksCsv != "" {
for _, s := range strings.Split(stacksCsv, ",") {
stacks = append(stacks, strings.TrimSpace(s))
}
}

var components []string
if componentsCsv != "" {
for _, c := range strings.Split(componentsCsv, ",") {
components = append(components, strings.TrimSpace(c))
}
}

// Get global flags from Viper (includes base-path, config, config-path, profile).
globalFlags := flags.ParseGlobalFlags(cmd, v)

// Build ConfigAndStacksInfo from global flags to honor config selection flags.
configAndStacksInfo := schema.ConfigAndStacksInfo{
AtmosBasePath: globalFlags.BasePath,
AtmosConfigFilesFromArg: globalFlags.Config,
AtmosConfigDirsFromArg: globalFlags.ConfigPath,
ProfilesFromArg: globalFlags.Profile,
}

// Initialize Atmos configuration.
atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true)
if err != nil {
return err
}

// Create adapter and service.
adapter := tfgenerate.NewExecAdapter(e.ProcessStacksForGenerate, e.FindStacksMapForGenerate)
service := tfgenerate.NewService(adapter)

// Execute based on mode.
if all {
return service.ExecuteForAll(&atmosConfig, stacks, components, dryRun, clean)
}

component := args[0]
return service.ExecuteForComponent(&atmosConfig, component, stack, dryRun, clean)
},
}

func init() {
// Create parser with files-specific flags using functional options.
filesParser = flags.NewStandardParser(
flags.WithStringFlag("stack", "s", "", "Stack name (required for single component)"),
flags.WithBoolFlag("all", "", false, "Process all components in all stacks"),
flags.WithStringFlag("stacks", "", "", "Filter stacks (glob pattern, requires --all)"),
flags.WithStringFlag("components", "", "", "Filter components (comma-separated, requires --all)"),
flags.WithBoolFlag("dry-run", "", false, "Show what would be generated without writing"),
flags.WithBoolFlag("clean", "", false, "Delete generated files instead of creating"),
flags.WithEnvVars("stack", "ATMOS_STACK"),
flags.WithEnvVars("stacks", "ATMOS_STACKS"),
flags.WithEnvVars("components", "ATMOS_COMPONENTS"),
)

// Register flags with the command.
filesParser.RegisterFlags(filesCmd)

// Bind flags to Viper for environment variable support.
if err := filesParser.BindToViper(viper.GetViper()); err != nil {
panic(err)
}

// Register with parent GenerateCmd.
GenerateCmd.AddCommand(filesCmd)
}
3 changes: 2 additions & 1 deletion cmd/terraform/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ This command supports the following subcommands:
- 'backends' to generate backend configuration files for all Atmos components in all stacks.
- 'varfile' to generate a variable file (varfile) for an Atmos component in a stack.
- 'varfiles' to generate varfiles for all Atmos components in all stacks.
- 'planfile' to generate a planfile for an Atmos component in a stack.`,
- 'planfile' to generate a planfile for an Atmos component in a stack.
- 'files' to generate files from the generate section for an Atmos component.`,
Args: cobra.NoArgs,
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
}
Loading
Loading