Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions cmd/terraform/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ as you would in a typical setup, but within the configured Atmos environment.`,
processFunctions := v.GetBool("process-functions")
skip := v.GetStringSlice("skip")
dryRun := v.GetBool("dry-run")
identity := v.GetString("identity")

// Prompt for stack if missing.
if stack == "" {
Expand Down Expand Up @@ -103,6 +104,7 @@ as you would in a typical setup, but within the configured Atmos environment.`,
Component: component,
Stack: stack,
DryRun: dryRun,
Identity: identity,
ProcessingOptions: e.ProcessingOptions{
ProcessTemplates: processTemplates,
ProcessFunctions: processFunctions,
Expand Down
20 changes: 19 additions & 1 deletion internal/exec/atmos.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,18 @@ func ExecuteAtmosCmd() error {
return nil
}

// All Terraform commands
// All Terraform commands.
if strings.HasPrefix(selectedCommand, "terraform") {
parts := strings.Split(selectedCommand, " ")
subcommand := parts[1]

// "terraform shell" is an Atmos-only command (not a native terraform subcommand).
// Route it directly to ExecuteTerraformShell to avoid ExecuteTerraform passing
// it to the terraform executable.
if subcommand == "shell" {
return ExecuteTerraformShell(shellOptionsForUI(selectedComponent, selectedStack), &atmosConfig)
}

configAndStacksInfo.ComponentType = "terraform"
configAndStacksInfo.Component = selectedComponent
configAndStacksInfo.ComponentFromArg = selectedComponent
Expand All @@ -170,3 +178,13 @@ func ExecuteAtmosCmd() error {

return nil
}

// shellOptionsForUI builds ShellOptions for the interactive UI dispatch path.
// The UI doesn't support DryRun or Identity selection, so those default to zero values.
func shellOptionsForUI(component, stack string) *ShellOptions {
return &ShellOptions{
Component: component,
Stack: stack,
ProcessingOptions: ProcessingOptions{ProcessTemplates: true, ProcessFunctions: true},
}
}
52 changes: 43 additions & 9 deletions internal/exec/terraform_shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exec

import (
"context"
"errors"
"time"

errUtils "github.com/cloudposse/atmos/errors"
Expand Down Expand Up @@ -35,22 +36,58 @@ func printShellDryRunInfo(info *schema.ConfigAndStacksInfo, cfg *shellConfig) {
ui.Writeln(" Varfile: " + cfg.varFile)
}

// shellInfoFromOptions builds a ConfigAndStacksInfo from ShellOptions.
func shellInfoFromOptions(opts *ShellOptions) schema.ConfigAndStacksInfo {
return schema.ConfigAndStacksInfo{
ComponentFromArg: opts.Component,
Stack: opts.Stack,
StackFromArg: opts.Stack,
ComponentType: "terraform",
SubCommand: "shell",
DryRun: opts.DryRun,
Identity: opts.Identity,
}
}

// resolveWorkdirPath returns the workdir path from componentSection if set by a workdir provisioner,
// otherwise returns the original componentPath unchanged.
func resolveWorkdirPath(componentSection map[string]any, componentPath string) string {
if workdirPath, ok := componentSection[provWorkdir.WorkdirPathKey].(string); ok && workdirPath != "" {
log.Debug("Using workdir path for shell", "workdirPath", workdirPath)
return workdirPath
}
return componentPath
}

// ExecuteTerraformShell starts an interactive shell configured for a terraform component.
func ExecuteTerraformShell(opts *ShellOptions, atmosConfig *schema.AtmosConfiguration) error {
defer perf.Track(atmosConfig, "exec.ExecuteTerraformShell")()

log.Debug("ExecuteTerraformShell called",
"component", opts.Component, "stack", opts.Stack,
"processTemplates", opts.ProcessTemplates, "processFunctions", opts.ProcessFunctions,
"skip", opts.Skip, "dryRun", opts.DryRun,
"skip", opts.Skip, "dryRun", opts.DryRun, "identity", opts.Identity,
)

info := schema.ConfigAndStacksInfo{
ComponentFromArg: opts.Component, Stack: opts.Stack, StackFromArg: opts.Stack,
ComponentType: "terraform", SubCommand: "shell", DryRun: opts.DryRun,
info := shellInfoFromOptions(opts)

// Create and authenticate AuthManager by merging global + component auth config.
// This enables YAML functions like !terraform.state to use authenticated credentials.
authManager, err := createAndAuthenticateAuthManager(atmosConfig, &info)
if err != nil {
// Special case: If user aborted (Ctrl+C), exit immediately without showing error.
if errors.Is(err, errUtils.ErrUserAborted) {
errUtils.Exit(errUtils.ExitCodeSIGINT)
}
return err
}

info, err := ProcessStacks(atmosConfig, info, true, opts.ProcessTemplates, opts.ProcessFunctions, opts.Skip, nil)
// Store AuthManager in configAndStacksInfo for YAML functions.
if authManager != nil {
info.AuthManager = authManager
}

info, err = ProcessStacks(atmosConfig, info, true, opts.ProcessTemplates, opts.ProcessFunctions, opts.Skip, authManager)
if err != nil {
return err
}
Expand All @@ -74,10 +111,7 @@ func ExecuteTerraformShell(opts *ShellOptions, atmosConfig *schema.AtmosConfigur
}

// Check if workdir provisioner set a workdir path - if so, use it instead of the component path.
if workdirPath, ok := info.ComponentSection[provWorkdir.WorkdirPathKey].(string); ok && workdirPath != "" {
componentPath = workdirPath
log.Debug("Using workdir path for shell", "workdirPath", workdirPath)
}
componentPath = resolveWorkdirPath(info.ComponentSection, componentPath)

cfg := &shellConfig{
componentPath: componentPath,
Expand Down
Loading
Loading