Skip to content

Commit 01604ac

Browse files
committed
Add -print flag
Add a -print flag which accepts no additional arguments and prints any found SSM env vars to stdout instead of exec-ing a process with the env vars set. ```sh -print Print the decrypted env vars without exporting them and exit ``` The use-case for this is in places like CI jobs where you may want to resolve SSM parameters and then write them to a config file, or persist them elsewhere for subsequent use. ssm-env is already a bit architecturally overloaded, and this strains it further. I'm not inclined to do a major refactor/rewrite at this point, but if we want to continue extending it that may be required at some point. I'd prob start by separating the interfaces for outputs and fallibility to avoid overloading the expandEnviron/setEnviron functions the way they currently are.
1 parent e0c837d commit 01604ac

File tree

2 files changed

+206
-37
lines changed

2 files changed

+206
-37
lines changed

main.go

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,30 +48,56 @@ func main() {
4848
template = flag.String("template", DefaultTemplate, "The template used to determine what the SSM parameter name is for an environment variable. When this template returns an empty string, the env variable is not an SSM parameter")
4949
decrypt = flag.Bool("with-decryption", false, "Will attempt to decrypt the parameter, and set the env var as plaintext")
5050
nofail = flag.Bool("no-fail", false, "Don't fail if error retrieving parameter")
51+
print = flag.Bool("print", false, "Print the decrypted env vars without exporting them and exit")
5152
)
5253
flag.Parse()
5354
args := flag.Args()
5455

55-
if len(args) <= 0 {
56+
if !*print && len(args) <= 0 {
5657
flag.Usage()
57-
os.Exit(1)
58+
fmt.Fprintf(os.Stderr, "\nmissing program to execute\n")
59+
os.Exit(2)
5860
}
5961

60-
path, err := exec.LookPath(args[0])
61-
must(err)
62+
if *print && len(args) > 0 {
63+
flag.Usage()
64+
fmt.Fprintf(os.Stderr, "\n-print is incompatible with arguments\n")
65+
os.Exit(3)
66+
}
6267

63-
var os osEnviron
68+
var osEnv osEnviron
6469

70+
// Construct the template we'll use for extracting the ssm params we need to
71+
// fetch.
6572
t, err := parseTemplate(*template)
6673
must(err)
74+
75+
// Construct an expander with the configs for fetching/replacing env vars.
6776
e := &expander{
6877
batchSize: defaultBatchSize,
6978
t: t,
7079
ssm: &lazySSMClient{},
71-
os: os,
80+
os: osEnv,
7281
}
73-
must(e.expandEnviron(*decrypt, *nofail))
74-
must(syscall.Exec(path, args[0:], os.Environ()))
82+
// Attempt to "expand" ssm vars.
83+
vars, err := e.expandEnviron(*decrypt, *nofail)
84+
must(err)
85+
86+
// Actually set the env vars for the process.
87+
e.setEnviron(*print, vars)
88+
// If -print was passed, we're done.
89+
if *print {
90+
os.Exit(0)
91+
}
92+
93+
// Make sure that we're invoking ssm-env with an executable that actually
94+
// exists.
95+
path, err := exec.LookPath(args[0])
96+
must(err)
97+
98+
// Exec whatever command was passed, using the current process' env vars
99+
// (which are now expanded).
100+
must(syscall.Exec(path, args[0:], osEnv.Environ()))
75101
}
76102

77103
// lazySSMClient wraps the AWS SDK SSM client such that the AWS session and
@@ -115,6 +141,9 @@ func (c *lazySSMClient) awsSession() (*session.Session, error) {
115141
return sess, nil
116142
}
117143

144+
// Construct the template we use for parsing out ssm env var strings (by
145+
// default, `DefaultTemplate`, which works with values like
146+
// "ssm://<path>:<version>").
118147
func parseTemplate(templateText string) (*template.Template, error) {
119148
return template.New("template").Funcs(TemplateFuncs).Parse(templateText)
120149
}
@@ -125,7 +154,9 @@ type ssmClient interface {
125154

126155
type environ interface {
127156
Environ() []string
128-
Setenv(key, vale string)
157+
Setenv(key, val string)
158+
Getenv(key string) string
159+
Write(s string) error
129160
}
130161

131162
type osEnviron int
@@ -138,6 +169,16 @@ func (e osEnviron) Setenv(key, val string) {
138169
os.Setenv(key, val)
139170
}
140171

172+
func (e osEnviron) Getenv(key string) string {
173+
return os.Getenv(key)
174+
}
175+
176+
func (e osEnviron) Write(s string) error {
177+
_, err := fmt.Println(s)
178+
179+
return err
180+
}
181+
141182
type ssmVar struct {
142183
envvar string
143184
parameter string
@@ -163,7 +204,22 @@ func (e *expander) parameter(k, v string) (*string, error) {
163204
return nil, nil
164205
}
165206

166-
func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
207+
func (e *expander) setEnviron(print bool, vars map[string]string) {
208+
// If -print was passed, just dump the decrypted env vars to stdout and return.
209+
if print {
210+
for k, v := range vars {
211+
e.os.Write(fmt.Sprintf("%s=%s", k, v))
212+
}
213+
214+
return
215+
}
216+
217+
for k, v := range vars {
218+
e.os.Setenv(k, v)
219+
}
220+
}
221+
222+
func (e *expander) expandEnviron(decrypt bool, nofail bool) (map[string]string, error) {
167223
// Environment variables that point to some SSM parameters.
168224
var ssmVars []ssmVar
169225

@@ -174,7 +230,7 @@ func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
174230
parameter, err := e.parameter(k, v)
175231
if err != nil {
176232
// TODO: Should this _also_ not error if nofail is passed?
177-
return fmt.Errorf("determining name of parameter: %v", err)
233+
return make(map[string]string), fmt.Errorf("determining name of parameter: %v", err)
178234
}
179235

180236
if parameter != nil {
@@ -185,16 +241,20 @@ func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
185241

186242
if len(uniqNames) == 0 {
187243
// Nothing to do, no SSM parameters.
188-
return nil
244+
return make(map[string]string), nil
189245
}
190246

247+
// Construct a string slice to hold each ssm value.
191248
names := make([]string, len(uniqNames))
249+
// Go through and extract the values from uniqNames into the string slice.
192250
i := 0
193251
for k := range uniqNames {
194252
names[i] = k
195253
i++
196254
}
197255

256+
// For each chunk of batched ssm params, get the decrypted values.
257+
decryptedVars := make(map[string]string)
198258
for i := 0; i < len(names); i += e.batchSize {
199259
j := i + e.batchSize
200260
if j > len(names) {
@@ -203,18 +263,26 @@ func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
203263

204264
values, err := e.getParameters(names[i:j], decrypt, nofail)
205265
if err != nil {
206-
return err
266+
return make(map[string]string), err
267+
}
268+
269+
if nofail && len(values) == 0 {
270+
for _, v := range ssmVars {
271+
decryptedVars[v.envvar] = e.os.Getenv(v.envvar)
272+
}
273+
274+
return decryptedVars, nil
207275
}
208276

209277
for _, v := range ssmVars {
210278
val, ok := values[v.parameter]
211279
if ok {
212-
e.os.Setenv(v.envvar, val)
280+
decryptedVars[v.envvar] = val
213281
}
214282
}
215283
}
216284

217-
return nil
285+
return decryptedVars, nil
218286
}
219287

220288
func (e *expander) getParameters(names []string, decrypt bool, nofail bool) (map[string]string, error) {
@@ -278,6 +346,7 @@ func splitVar(v string) (key, val string) {
278346
return parts[0], parts[1]
279347
}
280348

349+
// Abort with an error message if err is not nill.
281350
func must(err error) {
282351
if err != nil {
283352
fmt.Fprintf(os.Stderr, "ssm-env: %v\n", err)

0 commit comments

Comments
 (0)