-
-
Notifications
You must be signed in to change notification settings - Fork 153
feat: Add .env file support (DEV-2990) #1930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
268f81f
addb84d
1d620e3
7665bbe
55d116e
977181e
43f21ff
aaee4f2
12dd563
a29a98f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,9 +19,11 @@ import ( | |
| errUtils "github.com/cloudposse/atmos/errors" | ||
| "github.com/cloudposse/atmos/pkg/auth/provisioning" | ||
| "github.com/cloudposse/atmos/pkg/config/casemap" | ||
| "github.com/cloudposse/atmos/pkg/env" | ||
| "github.com/cloudposse/atmos/pkg/filesystem" | ||
| log "github.com/cloudposse/atmos/pkg/logger" | ||
| "github.com/cloudposse/atmos/pkg/schema" | ||
| "github.com/cloudposse/atmos/pkg/ui" | ||
| u "github.com/cloudposse/atmos/pkg/utils" | ||
| "github.com/cloudposse/atmos/pkg/version" | ||
| "github.com/cloudposse/atmos/pkg/xdg" | ||
|
|
@@ -276,6 +278,11 @@ func LoadConfig(configAndStacksInfo *schema.ConfigAndStacksInfo) (schema.AtmosCo | |
| } | ||
| setEnv(v) | ||
|
|
||
| // Early .env file loading: MUST happen before profile detection. | ||
| // This allows ATMOS_PROFILE and other ATMOS_* vars in .env files to influence Atmos behavior. | ||
| // The base path may not be fully resolved yet, so we use what's available from config. | ||
| loadEnvFilesEarly(v, v.GetString("base_path")) | ||
|
|
||
| // Load profiles if specified via --profile flag or ATMOS_PROFILE env var. | ||
| // Profiles are loaded after base config but before final unmarshaling. | ||
| // This allows profiles to override base config settings. | ||
|
|
@@ -325,9 +332,11 @@ func LoadConfig(configAndStacksInfo *schema.ConfigAndStacksInfo) (schema.AtmosCo | |
| // Both AtmosConfiguration.Env and Command.Env use "env" but with different types | ||
| // (map[string]string vs []CommandEnv), causing mapstructure to silently drop Commands. | ||
| // Using mapstructure:"-" on the Env fields and extracting manually here fixes this. | ||
| if envMap := v.GetStringMapString("env"); len(envMap) > 0 { | ||
| atmosConfig.Env = envMap | ||
| } | ||
| // | ||
| // The env section supports two forms: | ||
| // 1. Structured: env.vars (map) + env.files (EnvFilesConfig) | ||
| // 2. Flat (legacy): env as direct key-value map | ||
| parseEnvConfig(v, &atmosConfig) | ||
| if envMap := v.GetStringMapString("templates.settings.env"); len(envMap) > 0 { | ||
| atmosConfig.Templates.Settings.Env = envMap | ||
| } | ||
|
|
@@ -1231,10 +1240,93 @@ func loadEmbeddedConfig(v *viper.Viper) error { | |
| return nil | ||
| } | ||
|
|
||
| // parseEnvConfig extracts the env section from Viper, supporting both structured and flat forms. | ||
| // Structured form: env.vars (map) + env.files (EnvFilesConfig) | ||
| // Flat form (legacy): env as direct key-value map. | ||
| func parseEnvConfig(v *viper.Viper, atmosConfig *schema.AtmosConfiguration) { | ||
| // Check if env section uses structured or flat form. | ||
| envRaw := v.Get("env") | ||
| if envRaw == nil { | ||
| return | ||
| } | ||
|
|
||
| envMap, ok := envRaw.(map[string]any) | ||
| if !ok { | ||
| return | ||
| } | ||
|
|
||
| // Detect structured form by presence of "vars" or "files" keys. | ||
| _, hasVars := envMap["vars"] | ||
| _, hasFiles := envMap["files"] | ||
|
|
||
| if hasVars || hasFiles { | ||
| // Structured form: env.vars + env.files. | ||
| if hasVars { | ||
| atmosConfig.Env.Vars = v.GetStringMapString("env.vars") | ||
| } | ||
| // Files config is parsed via mapstructure into atmosConfig.Env.Files. | ||
| atmosConfig.Env.Files.Enabled = v.GetBool("env.files.enabled") | ||
| atmosConfig.Env.Files.Paths = v.GetStringSlice("env.files.paths") | ||
| atmosConfig.Env.Files.Parents = v.GetBool("env.files.parents") | ||
| } else { | ||
| // Flat form: all keys are vars (backward compatibility). | ||
| atmosConfig.Env.Vars = v.GetStringMapString("env") | ||
| } | ||
| } | ||
|
|
||
| // loadEnvFilesEarly loads .env files early in the config loading process. | ||
| // This must happen before profile detection to allow ATMOS_* variables | ||
| // from .env files to influence Atmos behavior. | ||
| // Returns the list of loaded files for UI feedback. | ||
| func loadEnvFilesEarly(v *viper.Viper, basePath string) []string { | ||
| // Check if env.files is enabled (quick bootstrap read). | ||
| if !v.GetBool("env.files.enabled") { | ||
| return nil | ||
| } | ||
|
|
||
| paths := v.GetStringSlice("env.files.paths") | ||
| if len(paths) == 0 { | ||
| paths = []string{".env"} // Default pattern. | ||
| } | ||
|
|
||
| // Determine base path for loading. | ||
| if basePath == "" { | ||
| basePath = v.GetString("base_path") | ||
| } | ||
| if basePath == "" { | ||
| basePath = "." | ||
| } | ||
|
|
||
| // Load .env files from base path. | ||
| envVars, loadedFiles, err := env.LoadEnvFiles(basePath, paths) | ||
| if err != nil { | ||
| log.Debug("Failed to load .env files", "error", err) | ||
|
Comment on lines
+1286
to
+1307
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's understand the context around lines 1286-1307 in pkg/config/load.go
cd /tmp && head -1320 /dev/stdin | tail -100 <<'EOF'
File will be read next
EOF
# Actually, let me read the file directly
head -1320 pkg/config/load.go | tail -100Repository: cloudposse/atmos Length of output: 247 🏁 Script executed: # Find the function definition around line 1286
sed -n '1250,1320p' pkg/config/load.go | cat -nRepository: cloudposse/atmos Length of output: 2763 🏁 Script executed: # Look for env.files.parents usage in the codebase
rg "env\.files\.parents" -A 3 -B 3Repository: cloudposse/atmos Length of output: 1157 🏁 Script executed: # Find the env.LoadEnvFiles function signature
rg "func.*LoadEnvFiles" -A 5Repository: cloudposse/atmos Length of output: 633 🏁 Script executed: # Search for the main loader that handles .env files
rg "LoadEnvFiles" -B 5 -A 10Repository: cloudposse/atmos Length of output: 8528 🏁 Script executed: # Check the entire function that contains lines 1286-1307
sed -n '1200,1350p' pkg/config/load.goRepository: cloudposse/atmos Length of output: 4722 🏁 Script executed: # Find LoadFromDirectory implementation
rg "LoadFromDirectory" -A 20Repository: cloudposse/atmos Length of output: 10461 🏁 Script executed: # Search for where the parents flag is actually used in env loading
rg "\.Parents" pkg/env/ -B 3 -A 3Repository: cloudposse/atmos Length of output: 42 🏁 Script executed: # Check if there's a broader function that respects parents
rg "parents" pkg/env/file.go -B 2 -A 5Repository: cloudposse/atmos Length of output: 834 🏁 Script executed: # Look for where the main loader (after profile detection) loads env files
rg "loadEnvFiles" -i -B 3 -A 3Repository: cloudposse/atmos Length of output: 7519 Early .env loader doesn't respect Currently, Update the early loader to use 🤖 Prompt for AI Agents |
||
| return nil | ||
| } | ||
|
|
||
| // Inject into process environment (don't override existing). | ||
| // This allows ATMOS_* vars to influence Atmos behavior. | ||
| for k, val := range envVars { | ||
| if _, exists := os.LookupEnv(k); !exists { | ||
| if err := os.Setenv(k, val); err != nil { //nolint:forbidigo // Intentional: setting env vars from .env files | ||
| log.Debug("Failed to set env var from .env file", "key", k, "error", err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Report loaded files via UI. | ||
| for _, file := range loadedFiles { | ||
| ui.Success(fmt.Sprintf("Loaded %s", filepath.Base(file))) | ||
| } | ||
|
|
||
| return loadedFiles | ||
| } | ||
|
|
||
| // caseSensitivePaths lists the YAML paths that need case preservation. | ||
| // Viper lowercases all map keys, but these sections need original case. | ||
| var caseSensitivePaths = []string{ | ||
| "env", // Environment variables (e.g., GITHUB_TOKEN) | ||
| "env", // Environment variables (e.g., GITHUB_TOKEN) - flat form | ||
| "env.vars", // Environment variables - structured form | ||
| "auth.identities", // Auth identity names (e.g., SuperAdmin) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 126
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 10707
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 401
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 2177
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 251
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 988
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1923
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 228
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 154
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 3224
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1829
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 634
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 275
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 3762
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 50373
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 771
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 988
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1389
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1366
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 3733
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 904
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 2024
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1169
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 112
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1244
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1710
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 2140
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 919
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 2140
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1346
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 635
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1814
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 42
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 334
Check value types before treating env config as structured form.
Legacy env configs with
VARSorFILESas flat keys will be misclassified as structured. Because Viper lowercases keys tovarsandfiles, the presence check succeeds even when those keys contain string values, not nested maps. ThenGetStringMapString("env.vars")returns empty, silently losing the data.✅ Proposed fix
🤖 Prompt for AI Agents