Skip to content

Commit e13aeb8

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 70be7a2 commit e13aeb8

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
@@ -50,6 +50,7 @@ func main() {
5050
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")
5151
decrypt = flag.Bool("with-decryption", false, "Will attempt to decrypt the parameter, and set the env var as plaintext")
5252
nofail = flag.Bool("no-fail", false, "Don't fail if error retrieving parameter")
53+
print = flag.Bool("print", false, "Print the decrypted env vars without exporting them and exit")
5354
print_version = flag.Bool("V", false, "Print the version and exit")
5455
)
5556
flag.Parse()
@@ -61,26 +62,51 @@ func main() {
6162
return
6263
}
6364

64-
if len(args) <= 0 {
65+
if !*print && len(args) <= 0 {
6566
flag.Usage()
66-
os.Exit(1)
67+
fmt.Fprintf(os.Stderr, "\nmissing program to execute\n")
68+
os.Exit(2)
6769
}
6870

69-
path, err := exec.LookPath(args[0])
70-
must(err)
71+
if *print && len(args) > 0 {
72+
flag.Usage()
73+
fmt.Fprintf(os.Stderr, "\n-print is incompatible with arguments\n")
74+
os.Exit(3)
75+
}
7176

72-
var os osEnviron
77+
var osEnv osEnviron
7378

79+
// Construct the template we'll use for extracting the ssm params we need to
80+
// fetch.
7481
t, err := parseTemplate(*template)
7582
must(err)
83+
84+
// Construct an expander with the configs for fetching/replacing env vars.
7685
e := &expander{
7786
batchSize: defaultBatchSize,
7887
t: t,
7988
ssm: &lazySSMClient{},
80-
os: os,
89+
os: osEnv,
8190
}
82-
must(e.expandEnviron(*decrypt, *nofail))
83-
must(syscall.Exec(path, args[0:], os.Environ()))
91+
// Attempt to "expand" ssm vars.
92+
vars, err := e.expandEnviron(*decrypt, *nofail)
93+
must(err)
94+
95+
// Actually set the env vars for the process.
96+
e.setEnviron(*print, vars)
97+
// If -print was passed, we're done.
98+
if *print {
99+
os.Exit(0)
100+
}
101+
102+
// Make sure that we're invoking ssm-env with an executable that actually
103+
// exists.
104+
path, err := exec.LookPath(args[0])
105+
must(err)
106+
107+
// Exec whatever command was passed, using the current process' env vars
108+
// (which are now expanded).
109+
must(syscall.Exec(path, args[0:], osEnv.Environ()))
84110
}
85111

86112
// lazySSMClient wraps the AWS SDK SSM client such that the AWS session and
@@ -124,6 +150,9 @@ func (c *lazySSMClient) awsSession() (*session.Session, error) {
124150
return sess, nil
125151
}
126152

153+
// Construct the template we use for parsing out ssm env var strings (by
154+
// default, `DefaultTemplate`, which works with values like
155+
// "ssm://<path>:<version>").
127156
func parseTemplate(templateText string) (*template.Template, error) {
128157
return template.New("template").Funcs(TemplateFuncs).Parse(templateText)
129158
}
@@ -134,7 +163,9 @@ type ssmClient interface {
134163

135164
type environ interface {
136165
Environ() []string
137-
Setenv(key, vale string)
166+
Setenv(key, val string)
167+
Getenv(key string) string
168+
Write(s string) error
138169
}
139170

140171
type osEnviron int
@@ -147,6 +178,16 @@ func (e osEnviron) Setenv(key, val string) {
147178
os.Setenv(key, val)
148179
}
149180

181+
func (e osEnviron) Getenv(key string) string {
182+
return os.Getenv(key)
183+
}
184+
185+
func (e osEnviron) Write(s string) error {
186+
_, err := fmt.Println(s)
187+
188+
return err
189+
}
190+
150191
type ssmVar struct {
151192
envvar string
152193
parameter string
@@ -172,7 +213,22 @@ func (e *expander) parameter(k, v string) (*string, error) {
172213
return nil, nil
173214
}
174215

175-
func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
216+
func (e *expander) setEnviron(print bool, vars map[string]string) {
217+
// If -print was passed, just dump the decrypted env vars to stdout and return.
218+
if print {
219+
for k, v := range vars {
220+
e.os.Write(fmt.Sprintf("%s=%s", k, v))
221+
}
222+
223+
return
224+
}
225+
226+
for k, v := range vars {
227+
e.os.Setenv(k, v)
228+
}
229+
}
230+
231+
func (e *expander) expandEnviron(decrypt bool, nofail bool) (map[string]string, error) {
176232
// Environment variables that point to some SSM parameters.
177233
var ssmVars []ssmVar
178234

@@ -183,7 +239,7 @@ func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
183239
parameter, err := e.parameter(k, v)
184240
if err != nil {
185241
// TODO: Should this _also_ not error if nofail is passed?
186-
return fmt.Errorf("determining name of parameter: %v", err)
242+
return make(map[string]string), fmt.Errorf("determining name of parameter: %v", err)
187243
}
188244

189245
if parameter != nil {
@@ -194,16 +250,20 @@ func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
194250

195251
if len(uniqNames) == 0 {
196252
// Nothing to do, no SSM parameters.
197-
return nil
253+
return make(map[string]string), nil
198254
}
199255

256+
// Construct a string slice to hold each ssm value.
200257
names := make([]string, len(uniqNames))
258+
// Go through and extract the values from uniqNames into the string slice.
201259
i := 0
202260
for k := range uniqNames {
203261
names[i] = k
204262
i++
205263
}
206264

265+
// For each chunk of batched ssm params, get the decrypted values.
266+
decryptedVars := make(map[string]string)
207267
for i := 0; i < len(names); i += e.batchSize {
208268
j := i + e.batchSize
209269
if j > len(names) {
@@ -212,18 +272,26 @@ func (e *expander) expandEnviron(decrypt bool, nofail bool) error {
212272

213273
values, err := e.getParameters(names[i:j], decrypt, nofail)
214274
if err != nil {
215-
return err
275+
return make(map[string]string), err
276+
}
277+
278+
if nofail && len(values) == 0 {
279+
for _, v := range ssmVars {
280+
decryptedVars[v.envvar] = e.os.Getenv(v.envvar)
281+
}
282+
283+
return decryptedVars, nil
216284
}
217285

218286
for _, v := range ssmVars {
219287
val, ok := values[v.parameter]
220288
if ok {
221-
e.os.Setenv(v.envvar, val)
289+
decryptedVars[v.envvar] = val
222290
}
223291
}
224292
}
225293

226-
return nil
294+
return decryptedVars, nil
227295
}
228296

229297
func (e *expander) getParameters(names []string, decrypt bool, nofail bool) (map[string]string, error) {
@@ -287,6 +355,7 @@ func splitVar(v string) (key, val string) {
287355
return parts[0], parts[1]
288356
}
289357

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

0 commit comments

Comments
 (0)