Internal InitCliConfig(schema.ConfigAndStacksInfo{}) re-invocations discard --config, breaking atmos terraform test/plan
Describe the Bug
atmos --config <file> terraform plan/test <component> -s <stack> fails with
Error: failed to find import, even with a single, complete, valid atmos.yaml — no
merging, no multiple files, nothing exotic. The exact same --config invocation works
fine for atmos --config <file> list stacks, so --config clearly is parsed and honored
initially. Something specific to terraform subcommands loses it partway through.
Confirmed via debug logs: the first config load correctly merges the --config-specified
file. A later, separate internal re-invocation of config loading — specific to
terraform-command processing — then runs plain auto-discovery from scratch (system dir, home
dir, parent dirs, CWD) with no trace of the original --config selection, finds nothing (since
this reproduction's only valid config lives at the --config-specified path, not anywhere
auto-discovery looks), and that's what actually fails.
Root cause, traced to source: Atmos's root command derives config selection from os.Args
correctly, once, at startup:
// cmd/root.go
atmosConfig, initErr = cfg.InitCliConfig(cfg.EarlyConfigAndStacksInfoFromArgs(os.Args[1:]), false)
But a large number of other call sites across the codebase call InitCliConfig with a bare,
empty struct literal instead of re-deriving or re-threading the original selection:
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
An empty schema.ConfigAndStacksInfo{} means AtmosConfigFilesFromArg, AtmosConfigDirsFromArg,
and AtmosBasePath are all zero-value — so that specific InitCliConfig call has no idea
--config was ever passed, and falls straight through to plain auto-discovery. This exact
anti-pattern appears in (non-exhaustive — found via a single code search, more likely exist):
pkg/vendoring/resolve.go (two call sites)
cmd/cmd_utils.go
pkg/ai/tools/atmos/stack_config.go
cmd/aws/eks/update_kubeconfig_sdk.go (two call sites)
pkg/devcontainer/config_interface.go (two call sites)
pkg/telemetry/utils.go (two call sites)
cmd/profile/show.go (two call sites)
This is why ATMOS_CLI_CONFIG_PATH (env var) does not have this problem: it's read directly
via os.Getenv() inside readEnvAmosConfigPath()/readSystemConfig() on every call, with no
struct field needing to be threaded through. --config's parsed value, by contrast, only
survives if whoever calls InitCliConfig() explicitly passes it along — and evidently, for
whatever specific internal step re-processes config during terraform plan/test, nobody does.
Expected Behavior
Once --config/--config-path/--base-path is specified for an Atmos invocation, every
internal reload of the CLI config during that same invocation should honor the same selection
— either by reusing the already-loaded AtmosConfiguration, or by re-deriving it via
EarlyConfigAndStacksInfoFromArgs(os.Args[1:]) (the same mechanism cmd/root.go already uses
correctly), rather than constructing a fresh, empty ConfigAndStacksInfo{}.
Steps to Reproduce
Minimal, self-contained repro. Creates a throwaway project in a temp dir with atmos.yaml
living in its own subdirectory (never auto-discoverable on its own), then compares list stacks against terraform plan for the identical --config invocation, plus two follow-up
cases isolating why. Copy-paste the whole block into a terminal:
REPRO_DIR="$(mktemp -d)"
echo "Reproducing in: $REPRO_DIR"
# Install atmos if not already on PATH (kept outside REPRO_DIR so atmos's own
# config-file discovery doesn't try to parse the binary itself as YAML)
if ! command -v atmos >/dev/null 2>&1; then
ATMOS_VERSION=1.225.0-rc.7
ATMOS_BIN_DIR="$(mktemp -d)"
curl -fsSL "https://github.com/cloudposse/atmos/releases/download/v${ATMOS_VERSION}/atmos_${ATMOS_VERSION}_linux_amd64" -o "$ATMOS_BIN_DIR/atmos"
chmod +x "$ATMOS_BIN_DIR/atmos"
ATMOS="$ATMOS_BIN_DIR/atmos"
else
ATMOS=atmos
fi
$ATMOS version
cd "$REPRO_DIR"
git init -q
mkdir -p config stacks/deploy components/terraform/my-component
cat > config/atmos.yaml <<'EOF'
base_path: ''
components:
terraform:
base_path: components/terraform
stacks:
base_path: stacks
included_paths:
- deploy/**/*
name_pattern: '{stage}'
EOF
cat > stacks/deploy/dev.yaml <<'EOF'
vars:
stage: dev
components:
terraform:
my-component:
vars: {}
EOF
cat > components/terraform/my-component/main.tf <<'EOF'
variable "stage" {
type = string
}
output "stage" {
value = var.stage
}
EOF
echo "CASE 1: --config, single complete file, list stacks (works - --config is honored)"
$ATMOS --config config/atmos.yaml list stacks
echo "CASE 2: identical --config, same single file, terraform plan (fails)"
$ATMOS --config config/atmos.yaml terraform plan my-component -s dev || true
echo "CASE 3: same file also placed as bare ./atmos.yaml in CWD - terraform plan now works,"
echo " because the internal re-invocation's auto-discovery happens to find THIS copy"
cp config/atmos.yaml ./atmos.yaml
$ATMOS --config config/atmos.yaml terraform plan my-component -s dev
rm ./atmos.yaml
echo "CASE 4: same file selected via ATMOS_CLI_CONFIG_PATH instead of --config - works,"
echo " because env vars are read fresh by every re-invocation, unlike the CLI flag"
ATMOS_CLI_CONFIG_PATH="$REPRO_DIR/config" $ATMOS terraform plan my-component -s dev
Case 1 prints dev. Case 2 fails with Error: failed to find import. Cases 3 and 4 both
succeed with a normal terraform plan output (+ stage = "dev").
Screenshots
No screenshots — CLI output only, included in Steps to Reproduce above. The exact failure
for Case 2:
Error: failed to find import
💡 Verify that base_path and stacks.base_path in atmos.yaml are correct
💡 If using ATMOS_BASE_PATH, ensure the path is correct relative to the working directory
💡 Verify stacks.base_path in atmos.yaml points to the correct directory
💡 Check that the stacks directory exists and contains stack configuration files
Environment
Additional Context
Suggested fix direction: audit every call site matching
InitCliConfig(schema.ConfigAndStacksInfo{}, ...) (or equivalent bare/empty construction) and
replace it with either the already-loaded AtmosConfiguration/ConfigAndStacksInfo from
earlier in the same command's execution, or a fresh call to
EarlyConfigAndStacksInfoFromArgs(os.Args[1:]) — the same pattern cmd/root.go already uses
correctly at startup. This is likely not limited to terraform/helmfile/packer — the same
anti-pattern was found in vendoring, AI tools, EKS kubeconfig, devcontainer, telemetry, and
profile-command code, so any of those may exhibit the same loss of --config context mid-command
under the right conditions.
Relationship to existing issues: distinct from #2863 (fixed by #2864 — base_path never
resolving to git root within a single LoadConfig() call for --config) and from #2867
(array-typed keys not merging correctly across multiple --config files). This issue
reproduces with exactly one --config file and no merging of any kind — the failure is a
downstream re-invocation losing the --config selection entirely, not a resolution or merge
defect within a single load.
Internal InitCliConfig(schema.ConfigAndStacksInfo{}) re-invocations discard --config, breaking atmos terraform test/plan
Describe the Bug
atmos --config <file> terraform plan/test <component> -s <stack>fails withError: failed to find import, even with a single, complete, validatmos.yaml— nomerging, no multiple files, nothing exotic. The exact same
--configinvocation worksfine for
atmos --config <file> list stacks, so--configclearly is parsed and honoredinitially. Something specific to
terraformsubcommands loses it partway through.Confirmed via debug logs: the first config load correctly merges the
--config-specifiedfile. A later, separate internal re-invocation of config loading — specific to
terraform-command processing — then runs plain auto-discovery from scratch (system dir, home
dir, parent dirs, CWD) with no trace of the original
--configselection, finds nothing (sincethis reproduction's only valid config lives at the
--config-specified path, not anywhereauto-discovery looks), and that's what actually fails.
Root cause, traced to source: Atmos's root command derives config selection from
os.Argscorrectly, once, at startup:
But a large number of other call sites across the codebase call
InitCliConfigwith a bare,empty struct literal instead of re-deriving or re-threading the original selection:
An empty
schema.ConfigAndStacksInfo{}meansAtmosConfigFilesFromArg,AtmosConfigDirsFromArg,and
AtmosBasePathare all zero-value — so that specificInitCliConfigcall has no idea--configwas ever passed, and falls straight through to plain auto-discovery. This exactanti-pattern appears in (non-exhaustive — found via a single code search, more likely exist):
pkg/vendoring/resolve.go(two call sites)cmd/cmd_utils.gopkg/ai/tools/atmos/stack_config.gocmd/aws/eks/update_kubeconfig_sdk.go(two call sites)pkg/devcontainer/config_interface.go(two call sites)pkg/telemetry/utils.go(two call sites)cmd/profile/show.go(two call sites)This is why
ATMOS_CLI_CONFIG_PATH(env var) does not have this problem: it's read directlyvia
os.Getenv()insidereadEnvAmosConfigPath()/readSystemConfig()on every call, with nostruct field needing to be threaded through.
--config's parsed value, by contrast, onlysurvives if whoever calls
InitCliConfig()explicitly passes it along — and evidently, forwhatever specific internal step re-processes config during
terraform plan/test, nobody does.Expected Behavior
Once
--config/--config-path/--base-pathis specified for an Atmos invocation, everyinternal reload of the CLI config during that same invocation should honor the same selection
— either by reusing the already-loaded
AtmosConfiguration, or by re-deriving it viaEarlyConfigAndStacksInfoFromArgs(os.Args[1:])(the same mechanismcmd/root.goalready usescorrectly), rather than constructing a fresh, empty
ConfigAndStacksInfo{}.Steps to Reproduce
Minimal, self-contained repro. Creates a throwaway project in a temp dir with
atmos.yamlliving in its own subdirectory (never auto-discoverable on its own), then compares
list stacksagainstterraform planfor the identical--configinvocation, plus two follow-upcases isolating why. Copy-paste the whole block into a terminal:
Case 1 prints
dev. Case 2 fails withError: failed to find import. Cases 3 and 4 bothsucceed with a normal terraform plan output (
+ stage = "dev").Screenshots
No screenshots — CLI output only, included in Steps to Reproduce above. The exact failure
for Case 2:
Environment
atmos terraform test#2863/fix(config): resolve git-root base_path for --config/--config-path #2864 fixand to --config with a conflicting array value in a second file makes stacks.included_paths unusable for stack discovery #2867; this reproduces with a single, complete
--configfile, no merging involved)--config— no custom commands, no unusual settingsAdditional Context
Suggested fix direction: audit every call site matching
InitCliConfig(schema.ConfigAndStacksInfo{}, ...)(or equivalent bare/empty construction) andreplace it with either the already-loaded
AtmosConfiguration/ConfigAndStacksInfofromearlier in the same command's execution, or a fresh call to
EarlyConfigAndStacksInfoFromArgs(os.Args[1:])— the same patterncmd/root.goalready usescorrectly at startup. This is likely not limited to
terraform/helmfile/packer— the sameanti-pattern was found in vendoring, AI tools, EKS kubeconfig, devcontainer, telemetry, and
profile-command code, so any of those may exhibit the same loss of
--configcontext mid-commandunder the right conditions.
Relationship to existing issues: distinct from #2863 (fixed by #2864 —
base_pathneverresolving to git root within a single
LoadConfig()call for--config) and from #2867(array-typed keys not merging correctly across multiple
--configfiles). This issuereproduces with exactly one
--configfile and no merging of any kind — the failure is adownstream re-invocation losing the
--configselection entirely, not a resolution or mergedefect within a single load.