-
-
Notifications
You must be signed in to change notification settings - Fork 153
feat: Add global env section to atmos.yaml #1829
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
Merged
Merged
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
eae6789
feat: Add global env section to atmos.yaml
osterman 778dac3
refactor: Address CodeRabbit review feedback
osterman 70311ff
refactor: Move mergeEnvVars functions to pkg/env package
osterman 08694c2
refactor: Move additional env functions to pkg/env package
osterman 577cfe6
fix: Use exact path matching in EnsureBinaryInPath
osterman baeccf2
feat(env): Add fluent builder pattern for constructing env slices
osterman 6134394
refactor(env): Remove wrapper functions, call env pkg directly
osterman b90c4fd
refactor(env): Remove convertEnvMapToSlice, use envpkg.ConvertMapToSlice
osterman 0e04b30
docs: Add CLI configuration page for global env section
osterman 4a96d09
docs: Add cross-references to stack-level env documentation
osterman ff892a2
docs: Use YAML functions instead of template functions in env docs
osterman 53aee88
docs: Fix GITHUB_TOKEN example to use !exec gh auth token
osterman daabe94
docs: Fix YAML function syntax in env documentation
osterman a7f6574
docs: Fix shell expansion and hyperlink issues in env documentation
osterman f5f8ca8
feat: Add `atmos env` command for outputting global environment varia…
osterman 0f12d7e
docs: Add demo-env example using GitHub provider with atmos env
osterman ef7026b
fix: Use name_template with vars.stage in demo-env example
osterman 226d008
fix: Update demo-env example to use available GitHub provider attributes
osterman 0ac2d43
refactor: Rename github-stars component to github-repo in demo-env
osterman a3b51ac
docs: Improve prerequisites in demo-env README
osterman 784156f
fix: Preserve case for env var names and strip trailing newlines from…
osterman abe3d8a
fix: Use unique heredoc delimiter and add security docs
osterman 52bc0f2
fix: env command respects CLI config overrides and improve logging
osterman 8edc265
fix: resolve mapstructure tag collision causing commands to be dropped
osterman 11471a3
fix: address CodeRabbit review feedback
osterman d8194e1
refactor: migrate env command to standard flags pattern
osterman 98704f8
fix: update Valid_Log_Level_in_Config_File snapshot for trace log
osterman 4181a18
Address CodeRabbit review comments
osterman 0596c2d
fix: regenerate help snapshots to include env command
osterman 4fcd860
fix: align remote import log level and fix test signature
osterman a5712a7
fix: regenerate non-existent command snapshot to include env
osterman 8260c57
Merge branch 'main' into osterman/global-env-section
osterman 66d70c4
Merge branch 'main' into osterman/global-env-section
aknysh 36863d8
fix: Use ErrorBuilder for config loading error in env command
aknysh 38c4613
test: Add tests to increase coverage for global env feature
aknysh 8c5761b
address comments, add tests
aknysh 4f6b2de
address comments, add tests
aknysh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| package env | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "slices" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/viper" | ||
|
|
||
| "github.com/cloudposse/atmos/cmd/internal" | ||
| errUtils "github.com/cloudposse/atmos/errors" | ||
| cfg "github.com/cloudposse/atmos/pkg/config" | ||
| "github.com/cloudposse/atmos/pkg/data" | ||
| "github.com/cloudposse/atmos/pkg/flags" | ||
| "github.com/cloudposse/atmos/pkg/flags/compat" | ||
| "github.com/cloudposse/atmos/pkg/schema" | ||
| u "github.com/cloudposse/atmos/pkg/utils" | ||
| ) | ||
|
|
||
| const ( | ||
| // DefaultFileMode is the file mode for output files. | ||
| defaultFileMode = 0o644 | ||
| ) | ||
|
|
||
| // SupportedFormats lists all supported output formats. | ||
| var SupportedFormats = []string{"bash", "json", "dotenv", "github"} | ||
|
|
||
| // envParser handles flag parsing with Viper precedence. | ||
| var envParser *flags.StandardParser | ||
|
|
||
| // envCmd outputs environment variables from atmos.yaml. | ||
| var envCmd = &cobra.Command{ | ||
| Use: "env", | ||
| Short: "Output environment variables configured in atmos.yaml", | ||
| Long: `Outputs environment variables from the 'env' section of atmos.yaml in various formats suitable for shell evaluation, .env files, JSON consumption, or GitHub Actions workflows.`, | ||
| Args: cobra.NoArgs, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| // Parse flags using Viper (respects precedence: flags > env > config > defaults). | ||
| v := viper.GetViper() | ||
| if err := envParser.BindFlagsToViper(cmd, v); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Get output format. | ||
| format := v.GetString("format") | ||
| if !slices.Contains(SupportedFormats, format) { | ||
| return errUtils.Build(errUtils.ErrInvalidArgumentError). | ||
| WithExplanationf("Invalid --format value %q.", format). | ||
| WithHintf("Supported formats: %s.", strings.Join(SupportedFormats, ", ")). | ||
| Err() | ||
| } | ||
|
|
||
| // Get output file path. | ||
| output := v.GetString("output") | ||
|
|
||
| // Build ConfigAndStacksInfo with CLI overrides (--config, --config-path, --base-path). | ||
| // These are persistent flags inherited from the root command. | ||
| configAndStacksInfo := schema.ConfigAndStacksInfo{} | ||
| if bp, _ := cmd.Flags().GetString("base-path"); bp != "" { | ||
| configAndStacksInfo.BasePath = bp | ||
| } | ||
| if cfgFiles, _ := cmd.Flags().GetStringSlice("config"); len(cfgFiles) > 0 { | ||
| configAndStacksInfo.AtmosConfigFilesFromArg = cfgFiles | ||
| } | ||
| if cfgDirs, _ := cmd.Flags().GetStringSlice("config-path"); len(cfgDirs) > 0 { | ||
| configAndStacksInfo.AtmosConfigDirsFromArg = cfgDirs | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Load atmos configuration (processStacks=false since env command doesn't require stack manifests). | ||
| atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, false) | ||
| if err != nil { | ||
| return errUtils.Build(errUtils.ErrMissingAtmosConfig). | ||
| WithCause(err). | ||
| WithExplanation("Failed to load atmos configuration."). | ||
| WithHint("Ensure atmos.yaml exists and is properly formatted."). | ||
| Err() | ||
| } | ||
|
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Get env vars with original case preserved (Viper lowercases all YAML map keys). | ||
| envVars := atmosConfig.GetCaseSensitiveMap("env") | ||
| if envVars == nil { | ||
| envVars = make(map[string]string) | ||
| } | ||
|
|
||
| // Handle GitHub format special case. | ||
| if format == "github" { | ||
| if output == "" { | ||
| // GITHUB_ENV is an external CI environment variable set by GitHub Actions, | ||
| // not an Atmos configuration variable, so os.Getenv is appropriate here. | ||
| //nolint:forbidigo // GITHUB_ENV is an external CI env var, not Atmos config | ||
| output = os.Getenv("GITHUB_ENV") | ||
| if output == "" { | ||
| return errUtils.Build(errUtils.ErrRequiredFlagNotProvided). | ||
| WithExplanation("--format=github requires GITHUB_ENV environment variable to be set, or use --output to specify a file path."). | ||
| Err() | ||
| } | ||
| } | ||
| return writeEnvToFile(envVars, output, formatGitHub) | ||
| } | ||
|
|
||
| // Handle file output for other formats. | ||
| if output != "" { | ||
| var formatter func(map[string]string) string | ||
| switch format { | ||
| case "bash": | ||
| formatter = formatBash | ||
| case "dotenv": | ||
| formatter = formatDotenv | ||
| case "json": | ||
| // For JSON file output, use the utility function. | ||
| return u.WriteToFileAsJSON(output, envVars, defaultFileMode) | ||
| default: | ||
| formatter = formatBash | ||
| } | ||
| return writeEnvToFile(envVars, output, formatter) | ||
| } | ||
|
|
||
| // Output to stdout. | ||
| switch format { | ||
| case "json": | ||
| return outputEnvAsJSON(&atmosConfig, envVars) | ||
| case "bash": | ||
| return outputEnvAsBash(envVars) | ||
| case "dotenv": | ||
| return outputEnvAsDotenv(envVars) | ||
| default: | ||
| return outputEnvAsBash(envVars) | ||
| } | ||
| }, | ||
| } | ||
|
|
||
| // outputEnvAsJSON outputs environment variables as JSON. | ||
| func outputEnvAsJSON(atmosConfig *schema.AtmosConfiguration, envVars map[string]string) error { | ||
| return u.PrintAsJSON(atmosConfig, envVars) | ||
| } | ||
|
|
||
| // outputEnvAsBash outputs environment variables as shell export statements. | ||
| func outputEnvAsBash(envVars map[string]string) error { | ||
| return data.Write(formatBash(envVars)) | ||
| } | ||
|
|
||
| // outputEnvAsDotenv outputs environment variables in .env format. | ||
| func outputEnvAsDotenv(envVars map[string]string) error { | ||
| return data.Write(formatDotenv(envVars)) | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // formatBash formats environment variables as shell export statements. | ||
| func formatBash(envVars map[string]string) string { | ||
| keys := sortedKeys(envVars) | ||
| var sb strings.Builder | ||
| for _, key := range keys { | ||
| value := envVars[key] | ||
| // Escape single quotes for safe single-quoted shell literals: ' -> '\''. | ||
| safe := strings.ReplaceAll(value, "'", "'\\''") | ||
| sb.WriteString(fmt.Sprintf("export %s='%s'\n", key, safe)) | ||
| } | ||
| return sb.String() | ||
| } | ||
|
|
||
| // formatDotenv formats environment variables in .env format. | ||
| func formatDotenv(envVars map[string]string) string { | ||
| keys := sortedKeys(envVars) | ||
| var sb strings.Builder | ||
| for _, key := range keys { | ||
| value := envVars[key] | ||
| // Use the same safe single-quoted escaping as bash output. | ||
| safe := strings.ReplaceAll(value, "'", "'\\''") | ||
| sb.WriteString(fmt.Sprintf("%s='%s'\n", key, safe)) | ||
| } | ||
| return sb.String() | ||
| } | ||
|
|
||
| // formatGitHub formats environment variables for GitHub Actions $GITHUB_ENV file. | ||
| // Uses KEY=value format without quoting. For multiline values, GitHub uses heredoc syntax. | ||
| func formatGitHub(envVars map[string]string) string { | ||
| keys := sortedKeys(envVars) | ||
| var sb strings.Builder | ||
| for _, key := range keys { | ||
| value := envVars[key] | ||
| // Check if value contains newlines - use heredoc syntax. | ||
| // Use ATMOS_EOF_ prefix to avoid collision with values containing "EOF". | ||
| if strings.Contains(value, "\n") { | ||
| sb.WriteString(fmt.Sprintf("%s<<ATMOS_EOF_%s\n%s\nATMOS_EOF_%s\n", key, key, value, key)) | ||
| } else { | ||
| sb.WriteString(fmt.Sprintf("%s=%s\n", key, value)) | ||
| } | ||
| } | ||
| return sb.String() | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // writeEnvToFile writes formatted environment variables to a file (append mode). | ||
| func writeEnvToFile(envVars map[string]string, filePath string, formatter func(map[string]string) string) error { | ||
| // Open file in append mode, create if doesn't exist. | ||
| f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, defaultFileMode) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to open file '%s': %w", filePath, err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| content := formatter(envVars) | ||
| if _, err := f.WriteString(content); err != nil { | ||
| return fmt.Errorf("failed to write to file '%s': %w", filePath, err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // sortedKeys returns the keys of a map sorted alphabetically. | ||
| func sortedKeys(m map[string]string) []string { | ||
| keys := make([]string, 0, len(m)) | ||
| for k := range m { | ||
| keys = append(keys, k) | ||
| } | ||
| sort.Strings(keys) | ||
| return keys | ||
| } | ||
|
|
||
| func init() { | ||
| // Create parser with env-specific flags using functional options. | ||
| envParser = flags.NewStandardParser( | ||
| flags.WithStringFlag("format", "f", "bash", "Output format: bash, json, dotenv, github"), | ||
| flags.WithStringFlag("output", "o", "", "Output file path (default: stdout, or $GITHUB_ENV for github format)"), | ||
| flags.WithEnvVars("format", "ATMOS_ENV_FORMAT"), | ||
| flags.WithEnvVars("output", "ATMOS_ENV_OUTPUT"), | ||
| ) | ||
|
|
||
| // Register flags using the standard RegisterFlags method. | ||
| envParser.RegisterFlags(envCmd) | ||
|
|
||
| // Bind flags to Viper for environment variable support. | ||
| if err := envParser.BindToViper(viper.GetViper()); err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| // Register format flag completion. | ||
| if err := envCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { | ||
| return SupportedFormats, cobra.ShellCompDirectiveNoFileComp | ||
| }); err != nil { | ||
| // Silently ignore completion registration errors. | ||
| _ = err | ||
| } | ||
|
|
||
| // Register this command with the registry. | ||
| internal.Register(&EnvCommandProvider{}) | ||
| } | ||
|
|
||
| // EnvCommandProvider implements the CommandProvider interface. | ||
| type EnvCommandProvider struct{} | ||
|
|
||
| // GetCommand returns the env command. | ||
| func (e *EnvCommandProvider) GetCommand() *cobra.Command { | ||
| return envCmd | ||
| } | ||
|
|
||
| // GetName returns the command name. | ||
| func (e *EnvCommandProvider) GetName() string { | ||
| return "env" | ||
| } | ||
|
|
||
| // GetGroup returns the command group for help organization. | ||
| func (e *EnvCommandProvider) GetGroup() string { | ||
| return "Configuration Management" | ||
| } | ||
|
|
||
| // GetFlagsBuilder returns the flags builder for this command. | ||
| func (e *EnvCommandProvider) GetFlagsBuilder() flags.Builder { | ||
| return envParser | ||
| } | ||
|
|
||
| // GetPositionalArgsBuilder returns the positional args builder for this command. | ||
| func (e *EnvCommandProvider) GetPositionalArgsBuilder() *flags.PositionalArgsBuilder { | ||
| return nil | ||
| } | ||
|
|
||
| // GetCompatibilityFlags returns compatibility flags for this command. | ||
| func (e *EnvCommandProvider) GetCompatibilityFlags() map[string]compat.CompatibilityFlag { | ||
| return nil | ||
| } | ||
|
|
||
| // GetAliases returns command aliases. | ||
| func (e *EnvCommandProvider) GetAliases() []internal.CommandAlias { | ||
| return nil | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.