Skip to content

Commit a1806c0

Browse files
ostermanclaude
andcommitted
feat: Implement component workdir provisioning and CLI commands
- Add WorkdirProvisioner to handle isolated working directories for component execution - Implement `atmos terraform workdir` commands: list, describe, show, clean - Workdirs prevent component instance conflicts by providing isolated execution spaces - Add workdir metadata tracking (source, hash, timestamps) - Integrate provisioner into terraform command pipeline - Add support for component workdir configuration in stack manifests 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 93d2e6b commit a1806c0

23 files changed

+4533
-1
lines changed

cmd/terraform/workdir/workdir.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package workdir
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/cloudposse/atmos/pkg/schema"
7+
)
8+
9+
// atmosConfigPtr will be set by SetAtmosConfig before command execution.
10+
var atmosConfigPtr *schema.AtmosConfiguration
11+
12+
// SetAtmosConfig sets the Atmos configuration for the workdir command.
13+
// This is called from root.go after atmosConfig is initialized.
14+
func SetAtmosConfig(config *schema.AtmosConfiguration) {
15+
atmosConfigPtr = config
16+
}
17+
18+
// workdirCmd represents the workdir command.
19+
var workdirCmd = &cobra.Command{
20+
Use: "workdir",
21+
Short: "Manage component working directories",
22+
Long: `List, describe, show, and clean component working directories.`,
23+
}
24+
25+
func init() {
26+
// Add subcommands.
27+
workdirCmd.AddCommand(listCmd)
28+
workdirCmd.AddCommand(describeCmd)
29+
workdirCmd.AddCommand(showCmd)
30+
workdirCmd.AddCommand(cleanCmd)
31+
}
32+
33+
// GetWorkdirCommand returns the workdir command for parent registration.
34+
func GetWorkdirCommand() *cobra.Command {
35+
return workdirCmd
36+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package workdir
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/spf13/viper"
9+
10+
errUtils "github.com/cloudposse/atmos/errors"
11+
cfg "github.com/cloudposse/atmos/pkg/config"
12+
"github.com/cloudposse/atmos/pkg/flags"
13+
"github.com/cloudposse/atmos/pkg/perf"
14+
"github.com/cloudposse/atmos/pkg/schema"
15+
"github.com/cloudposse/atmos/pkg/ui/theme"
16+
)
17+
18+
var cleanParser *flags.StandardParser
19+
20+
var cleanCmd = &cobra.Command{
21+
Use: "clean [component]",
22+
Short: "Clean workdir(s)",
23+
Long: `Remove component working directories.
24+
25+
Use --all to clean all workdirs, or specify a component and stack to clean a specific workdir.
26+
Workdirs are ephemeral and can be regenerated by running 'atmos terraform init'.`,
27+
Example: ` # Clean a specific workdir
28+
atmos terraform workdir clean vpc --stack dev
29+
30+
# Clean all workdirs
31+
atmos terraform workdir clean --all`,
32+
Args: cobra.MaximumNArgs(1),
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
defer perf.Track(atmosConfigPtr, "workdir.clean.RunE")()
35+
36+
v := viper.GetViper()
37+
all := v.GetBool("all")
38+
stack := v.GetString("stack")
39+
40+
// Validate arguments.
41+
if all && len(args) > 0 {
42+
return errUtils.Build(errUtils.ErrWorkdirClean).
43+
WithExplanation("Cannot specify both --all and a component").
44+
WithHint("Use --all alone to clean all workdirs, or specify a component and stack").
45+
Err()
46+
}
47+
48+
if !all && len(args) == 0 {
49+
return errUtils.Build(errUtils.ErrWorkdirClean).
50+
WithExplanation("Either --all or a component is required").
51+
WithHint("Use --all to clean all workdirs, or specify a component with --stack").
52+
Err()
53+
}
54+
55+
if !all && stack == "" {
56+
return errUtils.Build(errUtils.ErrWorkdirClean).
57+
WithExplanation("Stack is required when cleaning a specific workdir").
58+
WithHint("Use --stack to specify the stack").
59+
Err()
60+
}
61+
62+
// Initialize config.
63+
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
64+
if err != nil {
65+
return errUtils.Build(errUtils.ErrWorkdirClean).
66+
WithCause(err).
67+
WithExplanation("Failed to load atmos configuration").
68+
Err()
69+
}
70+
71+
// Clean workdirs.
72+
if all {
73+
return cleanAllWorkdirs(&atmosConfig)
74+
}
75+
76+
component := args[0]
77+
return cleanSpecificWorkdir(&atmosConfig, component, stack)
78+
},
79+
}
80+
81+
func cleanAllWorkdirs(atmosConfig *schema.AtmosConfiguration) error {
82+
if err := workdirManager.CleanAllWorkdirs(atmosConfig); err != nil {
83+
return err
84+
}
85+
86+
fmt.Fprintf(os.Stderr, "%s All workdirs cleaned\n", theme.Styles.Checkmark.String())
87+
return nil
88+
}
89+
90+
func cleanSpecificWorkdir(atmosConfig *schema.AtmosConfiguration, component, stack string) error {
91+
if err := workdirManager.CleanWorkdir(atmosConfig, component, stack); err != nil {
92+
return err
93+
}
94+
95+
fmt.Fprintf(os.Stderr, "%s Workdir cleaned for %s in %s\n", theme.Styles.Checkmark.String(), component, stack)
96+
return nil
97+
}
98+
99+
func init() {
100+
cleanCmd.DisableFlagParsing = false
101+
102+
// Create parser with functional options.
103+
cleanParser = flags.NewStandardParser(
104+
flags.WithStackFlag(),
105+
flags.WithBoolFlag("all", "a", false, "Clean all workdirs"),
106+
flags.WithEnvVars("all", "ATMOS_WORKDIR_CLEAN_ALL"),
107+
)
108+
109+
// Register flags with the command.
110+
cleanParser.RegisterFlags(cleanCmd)
111+
112+
// Bind flags to Viper.
113+
if err := cleanParser.BindToViper(viper.GetViper()); err != nil {
114+
panic(err)
115+
}
116+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package workdir
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/spf13/viper"
8+
9+
errUtils "github.com/cloudposse/atmos/errors"
10+
cfg "github.com/cloudposse/atmos/pkg/config"
11+
"github.com/cloudposse/atmos/pkg/flags"
12+
"github.com/cloudposse/atmos/pkg/perf"
13+
"github.com/cloudposse/atmos/pkg/schema"
14+
)
15+
16+
var describeParser *flags.StandardParser
17+
18+
var describeCmd = &cobra.Command{
19+
Use: "describe <component>",
20+
Short: "Describe workdir as stack manifest",
21+
Long: `Output the workdir configuration as a valid Atmos stack manifest snippet.
22+
23+
The output can be used to see or copy the workdir configuration in a format
24+
that matches the stack manifest structure.`,
25+
Example: ` atmos terraform workdir describe vpc --stack dev`,
26+
Args: cobra.ExactArgs(1),
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
defer perf.Track(atmosConfigPtr, "workdir.describe.RunE")()
29+
30+
component := args[0]
31+
32+
v := viper.GetViper()
33+
stack := v.GetString("stack")
34+
35+
if stack == "" {
36+
return errUtils.Build(errUtils.ErrWorkdirMetadata).
37+
WithExplanation("Stack is required").
38+
WithHint("Use --stack to specify the stack").
39+
Err()
40+
}
41+
42+
// Initialize config.
43+
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
44+
if err != nil {
45+
return errUtils.Build(errUtils.ErrWorkdirMetadata).
46+
WithCause(err).
47+
WithExplanation("Failed to load atmos configuration").
48+
Err()
49+
}
50+
51+
// Get workdir manifest.
52+
manifest, err := workdirManager.DescribeWorkdir(&atmosConfig, component, stack)
53+
if err != nil {
54+
return err
55+
}
56+
57+
// Output the manifest.
58+
fmt.Print(manifest)
59+
return nil
60+
},
61+
}
62+
63+
func init() {
64+
describeCmd.DisableFlagParsing = false
65+
66+
// Create parser with functional options.
67+
describeParser = flags.NewStandardParser(
68+
flags.WithStackFlag(),
69+
)
70+
71+
// Register flags with the command.
72+
describeParser.RegisterFlags(describeCmd)
73+
74+
// Bind flags to Viper.
75+
if err := describeParser.BindToViper(viper.GetViper()); err != nil {
76+
panic(err)
77+
}
78+
}

0 commit comments

Comments
 (0)