-
-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathenv.go
More file actions
285 lines (248 loc) · 9.29 KB
/
env.go
File metadata and controls
285 lines (248 loc) · 9.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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
}
// 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()
}
// 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))
}
// 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()
}
// 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
}