Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
59e8305
feat: Add interactive file generation for terraform, helmfile, and pa…
osterman Jan 15, 2026
b320973
fix: Address CodeRabbit review feedback for file generation
osterman Jan 16, 2026
bf8d40b
docs: Add blog post and roadmap update for interactive file generation
osterman Jan 16, 2026
5251240
Merge remote-tracking branch 'origin/main' into osterman/generate-exa…
osterman Jan 16, 2026
50b620b
docs: Add EmbedExample to generate files documentation
osterman Jan 16, 2026
0e8ed96
test: Add CLI tests for terraform generate files command
osterman Jan 16, 2026
68b0dfc
docs: Add EmbedExample to devcontainer documentation
osterman Jan 16, 2026
cf88fbd
chore: Add generate-files example to file-browser plugin
osterman Jan 16, 2026
fc36e60
chore: Remove redundant Go test file for terraform generate files
osterman Jan 16, 2026
25f252e
test: Add YAML tests for generate-files example
osterman Jan 16, 2026
b16349e
test: Add file creation validation tests for generate-files example
osterman Jan 16, 2026
f84af13
Merge remote-tracking branch 'origin/main' into osterman/generate-exa…
osterman Jan 17, 2026
cb95083
test: Regenerate snapshots after merging main
osterman Jan 17, 2026
6446c14
Merge remote-tracking branch 'origin/main' into osterman/generate-exa…
osterman Jan 17, 2026
e22e25d
fix: Address CodeRabbit review feedback
osterman Jan 17, 2026
e2d4adc
Merge remote-tracking branch 'origin/main' into osterman/generate-exa…
osterman Jan 18, 2026
0030287
Merge remote-tracking branch 'origin/main' into osterman/generate-exa…
osterman Jan 21, 2026
9a08ab1
Merge remote-tracking branch 'origin/main' into osterman/generate-exa…
osterman Jan 21, 2026
4bfef8f
fix: Address CodeRabbit review feedback for file generator
osterman Jan 21, 2026
e22f4e4
Merge branch 'main' into osterman/generate-example
aknysh Jan 22, 2026
0b17b5e
address comments, add tests
aknysh Jan 22, 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: 35 additions & 6 deletions cmd/terraform/generate/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/cloudposse/atmos/cmd/terraform/shared"
errUtils "github.com/cloudposse/atmos/errors"
e "github.com/cloudposse/atmos/internal/exec"
cfg "github.com/cloudposse/atmos/pkg/config"
Expand Down Expand Up @@ -50,15 +51,41 @@ The generate section in stack configuration supports:
dryRun := v.GetBool("dry-run")
clean := v.GetBool("clean")

// Validate: component requires stack, --all excludes component.
// Validate: --all excludes component argument.
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)

// Get component from args or prompt for it (when not using --all).
var component string
if len(args) > 0 {
component = args[0]
} else if !all {
// Prompt for component if missing.
prompted, err := shared.PromptForComponent(cmd, "")
if err = shared.HandlePromptError(err, "component"); err != nil {
return err
}
component = prompted
}
if len(args) == 0 && !all {
return fmt.Errorf("%w: either specify a component or use --all", errUtils.ErrInvalidFlag)

// Validate component after prompting (required when not using --all).
if !all && component == "" {
return errUtils.ErrMissingComponent
}

// Prompt for stack if missing (required when component is specified).
if component != "" && stack == "" {
prompted, err := shared.PromptForStack(cmd, component)
if err = shared.HandlePromptError(err, "stack"); err != nil {
return err
}
stack = prompted
}

// Validate stack after prompting (required when component is specified).
if component != "" && stack == "" {
return errUtils.ErrMissingStack
}

// Parse CSV values with whitespace trimming.
Expand Down Expand Up @@ -102,7 +129,6 @@ The generate section in stack configuration supports:
return service.ExecuteForAll(&atmosConfig, stacks, components, dryRun, clean)
}

component := args[0]
return service.ExecuteForComponent(&atmosConfig, component, stack, dryRun, clean)
},
}
Expand All @@ -129,6 +155,9 @@ func init() {
panic(err)
}

// Register shell completion for component.
filesCmd.ValidArgsFunction = shared.ComponentsArgCompletion

// Register with parent GenerateCmd.
GenerateCmd.AddCommand(filesCmd)
}
66 changes: 66 additions & 0 deletions cmd/terraform/generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,69 @@ func TestVarfileValidation(t *testing.T) {
assert.ErrorIs(t, err, errUtils.ErrMissingComponent)
})
}

// TestFilesValidation tests validation errors in the files command.
func TestFilesValidation(t *testing.T) {
t.Run("component with all flag returns error", func(t *testing.T) {
// Get the global viper instance and set up clean state.
v := viper.GetViper()
// Reset flags that might interfere.
v.Set("all", true)
v.Set("stack", "test-stack") // Provide stack to avoid that error.
defer func() {
// Clean up.
v.Set("all", false)
v.Set("stack", "")
}()

// Create a test command to execute RunE directly.
cmd := &cobra.Command{Use: "files"}
filesParser.RegisterFlags(cmd)

// Execute RunE with component argument and --all flag set in viper.
err := filesCmd.RunE(cmd, []string{"my-component"})
assert.ErrorIs(t, err, errUtils.ErrInvalidFlag)
})

t.Run("missing component without all flag returns error", func(t *testing.T) {
// Get the global viper instance and set up clean state.
v := viper.GetViper()
v.Set("all", false)
v.Set("stack", "")
defer func() {
// Clean up.
v.Set("all", false)
v.Set("stack", "")
}()

// Create a test command to execute RunE directly.
cmd := &cobra.Command{Use: "files"}
filesParser.RegisterFlags(cmd)

// Execute RunE with no component argument and no --all flag.
// In non-TTY environment, the prompt returns ErrInteractiveModeNotAvailable.
err := filesCmd.RunE(cmd, []string{})
assert.ErrorIs(t, err, errUtils.ErrMissingComponent)
})

t.Run("component without stack returns error", func(t *testing.T) {
// Get the global viper instance and set up clean state.
v := viper.GetViper()
v.Set("all", false)
v.Set("stack", "")
defer func() {
// Clean up.
v.Set("all", false)
v.Set("stack", "")
}()

// Create a test command to execute RunE directly.
cmd := &cobra.Command{Use: "files"}
filesParser.RegisterFlags(cmd)

// Execute RunE with component argument but no stack.
// In non-TTY environment, the prompt returns ErrInteractiveModeNotAvailable.
err := filesCmd.RunE(cmd, []string{"my-component"})
assert.ErrorIs(t, err, errUtils.ErrMissingStack)
})
}
8 changes: 8 additions & 0 deletions examples/generate-files/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Components folder (created during example usage)
components/

# Terraform
.terraform/
*.tfstate*
*.tfvars.json
backend.tf.json
182 changes: 182 additions & 0 deletions examples/generate-files/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Generate Files

This example demonstrates how to use `atmos terraform generate files` to generate an entire Terraform component from stack configuration.

## Overview

The `generate` section in component configuration defines files that Atmos will create. This example generates a complete Terraform component including:

- `variables.tf` - Variable definitions
- `outputs.tf` - Output definitions
- `versions.tf` - Terraform version constraints
- `locals.tf` - Local values
- `terraform.tfvars` - Variable values
- `config.json` - Environment-specific configuration
- `README.md` - Component documentation

## Content Types

| Content Type | Behavior |
|--------------|----------|
| String (multiline) | Written as-is, supports Go templates |
| Map with `.json` extension | Serialized as pretty-printed JSON |
| Map with `.yaml`/`.yml` extension | Serialized as YAML |
| Map with `.tf`/`.hcl` extension | Serialized as HCL blocks |
| Map with `.tfvars` extension | Serialized as HCL attributes (no blocks) |

### HCL Generation

When using map syntax for `.tf` files, Atmos automatically handles:

- **Labeled blocks** (`variable`, `output`, `resource`, `data`, `module`, `provider`):
```yaml
variable:
app_name:
type: string
description: "Application name"
```
Generates: `variable "app_name" { type = "string" ... }`

- **Unlabeled blocks** (`terraform`, `locals`):
```yaml
terraform:
required_version: ">= 1.0.0"
```
Generates: `terraform { required_version = ">= 1.0.0" }`

**Note:** For blocks that need HCL expressions (like `value = var.app_name`), use string templates instead of map syntax.

## Template Variables

Available in templates via `{{ .variable }}`:

| Variable | Description |
|----------|-------------|
| `atmos_component` | Component name |
| `atmos_stack` | Stack name |
| `vars` | Component variables (map) |
| `vars.stage` | Stage from component vars |

## Auto-Generation

Enable automatic file generation on any terraform command:

```yaml
# atmos.yaml
components:
terraform:
auto_generate_files: true
```

With this enabled, files are regenerated before `init`, `plan`, `apply`, etc.

## Usage

### Generate the component

```bash
cd examples/generate-files
mkdir -p components/terraform/demo
atmos terraform generate files demo -s dev
```

### Preview without writing (dry-run)

```bash
atmos terraform generate files demo -s dev --dry-run
```

### Delete generated files (clean)

```bash
atmos terraform generate files demo -s dev --clean
```

## Generated Files

After running `atmos terraform generate files demo -s dev`:

**versions.tf** (HCL map syntax):
```hcl
terraform {
required_version = ">= 1.0.0"
}
```

**locals.tf** (HCL map syntax with templates):
```hcl
locals {
app_name = "myapp-dev"
environment = "dev"
}
```

**variables.tf** (string template):
```hcl
variable "app_name" {
type = string
description = "Application name"
}
```

**terraform.tfvars** (flat HCL attributes):
```hcl
app_name = "myapp-dev"
version = "1.0.0-dev"
```

**config.json** (JSON from map):
```json
{
"app": "myapp-dev",
"stage": "dev",
"version": "1.0.0-dev"
}
```

## Project Structure

```text
generate-files/
├── atmos.yaml # Atmos configuration (auto_generate_files: true)
├── components/ # Generated (gitignored)
│ └── terraform/
│ └── demo/
│ ├── variables.tf # Generated
│ ├── outputs.tf # Generated
│ ├── versions.tf # Generated
│ ├── locals.tf # Generated
│ ├── terraform.tfvars # Generated
│ ├── config.json # Generated
│ └── README.md # Generated
└── stacks/
├── catalog/
│ └── demo.yaml # Component with generate section
└── deploy/
├── dev.yaml # Dev environment
└── prod.yaml # Prod environment
```

## Try It

```bash
cd examples/generate-files

# Create component directory
mkdir -p components/terraform/demo

# Generate files for dev
atmos terraform generate files demo -s dev

# See what was generated
ls components/terraform/demo/
cat components/terraform/demo/versions.tf
cat components/terraform/demo/locals.tf

# Generate for prod (different values)
atmos terraform generate files demo -s prod
cat components/terraform/demo/config.json

# Clean up
atmos terraform generate files demo -s dev --clean
```
21 changes: 21 additions & 0 deletions examples/generate-files/atmos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Minimal Atmos configuration for file generation demo
base_path: "./"

components:
terraform:
base_path: "components/terraform"
apply_auto_approve: false
deploy_run_init: true
auto_generate_files: true

stacks:
base_path: "stacks"
included_paths:
- "deploy/**/*"
excluded_paths:
- "**/_defaults.yaml"
name_template: "{{ .vars.stage }}"

logs:
level: Info
file: "/dev/stderr"
Loading
Loading