├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── data.go ├── data_test.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── runner.go └── ssm.go /.gitignore: -------------------------------------------------------------------------------- 1 | env-aws-params 2 | vendor 3 | .idea 4 | coverage.txt 5 | target/* 6 | .vscode/* 7 | **.code-workspace -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11-alpine AS builder 2 | WORKDIR /go/src/github.com/gmr/env-aws-params 3 | COPY . . 4 | RUN apk add git make\ 5 | && go get -u github.com/golang/dep/cmd/dep \ 6 | && make all 7 | 8 | FROM alpine:3.8 9 | COPY --from=builder /go/src/github.com/gmr/env-aws-params/env-aws-params / 10 | RUN apk add --no-cache ca-certificates 11 | 12 | ENTRYPOINT [ "/env-aws-params" ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2023, Gavin M. Roy, AWeber Communications 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG_NAME := env-aws-params 2 | PLATFORMS := linux-amd64 linux-arm64 darwin-amd64 3 | VERSION ?= 0.0.0 4 | 5 | TARGETS = $(addprefix target/$(PKG_NAME)_,$(PLATFORMS)) 6 | 7 | GO := go 8 | RM ?= rm 9 | 10 | # some macros to parse that platforms 11 | os = $(word 1,$(subst -, , $@)) 12 | arch = $(word 2,$(subst -, , $@)) 13 | platform = $(word 2,$(subst _, , $@)) 14 | 15 | all: build 16 | 17 | clean: 18 | @ $(GO) clean 19 | @ $(RM) -fr target/ 20 | 21 | deps: 22 | $(GO) mod download 23 | $(GO) mod verify 24 | 25 | 26 | $(PLATFORMS): deps 27 | GOOS=$(os) GOARCH=$(arch) $(GO) build -ldflags "-w -s -X main.VersionString=$(VERSION)" -o target/$(PKG_NAME)_$@ 28 | 29 | $(TARGETS): go.mod 30 | make $(platform) 31 | 32 | test: deps 33 | $(GO) test 34 | 35 | fmt: 36 | $(GO) fmt 37 | 38 | build: fmt deps test $(TARGETS) 39 | 40 | .PHONY: all clean deps test fmt build 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # env-aws-params 2 | 3 | [![Build Status](https://travis-ci.org/gmr/env-aws-params.svg?branch=master)](https://travis-ci.org/gmr/env-aws-params) 4 | 5 | ``env-aws-params`` is a tool that injects AWS EC2 Systems Manager (SSM) 6 | [Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html) 7 | Key / Value pairs as [Environment Variables](https://en.wikipedia.org/wiki/Environment_variable) 8 | when executing an application. It is intended to be used as a Docker 9 | [Entrypoint](https://docs.docker.com/engine/reference/builder/#entrypoint), 10 | but can really be used to launch applications outside of Docker as well. 11 | 12 | The primary goal is to provide a way of injecting environment variables for 13 | [12 Factor](https://12factor.net) applications that have their configuration defined 14 | in the SSM Parameter store. It was directly inspired by 15 | [envconsul](https://github.com/hashicorp/envconsul). 16 | 17 | ## Example Usage 18 | 19 | Create parameters in Parameter Store: 20 | ```bash 21 | aws ssm put-parameter --name /service-prefix/ENV_VAR1 --value example 22 | aws ssm put-parameter --name /service-prefix/ENV_VAR2 --value test-value 23 | ``` 24 | 25 | Then use ``env-aws-params`` to have bash display the env vars it was called with: 26 | ```bash 27 | env-aws-params --prefix /service-prefix /bin/bash -c set 28 | ``` 29 | 30 | If you want to include common and service specific values, ``--prefix`` can be specified 31 | multiple times: 32 | ```bash 33 | env-aws-params --prefix /common /bin/bash -c set 34 | ``` 35 | 36 | To get a plaintext output of your environment variables to use with other utilities, we can use `printenv`: 37 | ```bash 38 | env-aws-params --pristine --silent --prefix /service-prefix /usr/bin/printenv > ~/some-file.sh 39 | ``` 40 | Which will write your environment variables in plain text, for example: 41 | ```bash 42 | # ~/some-file.sh Contents: 43 | ENV_VAR1=example 44 | ENV_VAR2=test-value 45 | ``` 46 | 47 | ## CLI Options 48 | 49 | ``` 50 | NAME: 51 | env-aws-params - Application entry-point that injects SSM Parameter Store values as Environment Variables 52 | 53 | USAGE: 54 | env-aws-params [global options] -p prefix command [command arguments] 55 | 56 | COMMANDS: 57 | help, h Shows a list of commands or help for one command 58 | 59 | GLOBAL OPTIONS: 60 | --aws-region value The AWS region to use for the Parameter Store API [$AWS_REGION] 61 | --prefix value, -p value Key prefix that is used to retrieve the environment variables - supports multiple use 62 | --pristine Only use values retrieved from Parameter Store, do not inherit the existing environment variables 63 | --sanitize Replace invalid characters in keys to underscores 64 | --strip Strip invalid characters in keys 65 | --upcase Force keys to uppercase 66 | --debug Log additional debugging information [$PARAMS_DEBUG] 67 | --silent Silence all logs [$PARAMS_SILENT] 68 | --help, -h show help 69 | --version, -v print the version 70 | ``` 71 | 72 | ## Building 73 | 74 | This project uses [go modules](https://go.dev/blog/using-go-modules). To build the project: 75 | 76 | ```bash 77 | go mod download 78 | go mod verify 79 | go build 80 | ``` 81 | 82 | Building an environment is also provided as a docker image based on Alpine Linux. See the Dockerfile for more information. 83 | 84 | ```bash 85 | docker build -t env-aws-params; # Build the image 86 | docker run --rm -it -v $HOME/.aws/:/root/.aws/ env-aws-params [your options] 87 | ``` 88 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | var InvalidPattern = regexp.MustCompile(`[^a-zA-Z0-9_]`) 11 | 12 | func BuildEnvVars(parameters map[string]string, sanitize bool, strip bool, upcase bool) []string { 13 | var vars []string 14 | 15 | for k, v := range parameters { 16 | if sanitize == true { 17 | k = InvalidPattern.ReplaceAllString(k, "_") 18 | } 19 | if strip == true { 20 | k = InvalidPattern.ReplaceAllString(k, "") 21 | } 22 | if upcase == true { 23 | k = strings.ToUpper(k) 24 | } 25 | vars = append(vars, fmt.Sprintf("%s=%s", k, v)) 26 | } 27 | sort.Strings(vars) 28 | return vars 29 | } 30 | -------------------------------------------------------------------------------- /data_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func AssertEqual(t *testing.T, value []string, expect []string) { 10 | if len(value) != len(expect) { 11 | t.Error("Slices are not the same length", len(value), len(expect)) 12 | } 13 | sort.Strings(value) 14 | sort.Strings(expect) 15 | for i, _ := range value { 16 | if value[i] != expect[i] { 17 | t.Error(fmt.Sprintf("Values at offset %v do not match", i), value[i], expect[i]) 18 | } 19 | } 20 | } 21 | 22 | func TestBuildEnvVarsUpcaseFalse(t *testing.T) { 23 | var params map[string]string 24 | 25 | params = make(map[string]string) 26 | params["baz"] = "qux" 27 | params["FOO"] = "bar" 28 | 29 | expectation := []string{"baz=qux", "FOO=bar"} 30 | envvars := BuildEnvVars(params, false, false, false) 31 | AssertEqual(t, envvars, expectation) 32 | } 33 | 34 | func TestBuildEnvVarsUpcaseTrue(t *testing.T) { 35 | var params map[string]string 36 | 37 | params = make(map[string]string) 38 | params["baz"] = "qux" 39 | params["FOO"] = "bar" 40 | 41 | expectation := []string{"BAZ=qux", "FOO=bar"} 42 | envVars := BuildEnvVars(params, false, false, true) 43 | AssertEqual(t, envVars, expectation) 44 | } 45 | 46 | func TestBuildEnvVarsUpperSanitize(t *testing.T) { 47 | var params map[string]string 48 | 49 | params = make(map[string]string) 50 | params["FOO"] = "bar" 51 | params["baz-corgie"] = "qux" 52 | params["wE_irD-kEY!"] = "zaphod" 53 | 54 | expectation := []string{"BAZ_CORGIE=qux", "FOO=bar", "WE_IRD_KEY_=zaphod"} 55 | envVars := BuildEnvVars(params, true, false, true) 56 | AssertEqual(t, envVars, expectation) 57 | } 58 | 59 | func TestBuildEnvVarsUpperStrip(t *testing.T) { 60 | var params map[string]string 61 | 62 | params = make(map[string]string) 63 | params["FOO"] = "bar" 64 | params["baz-corgie"] = "qux" 65 | params["wE_irD-kEY!"] = "zaphod" 66 | 67 | expectation := []string{"BAZCORGIE=qux", "FOO=bar", "WE_IRDKEY=zaphod"} 68 | envVars := BuildEnvVars(params, false, true, true) 69 | AssertEqual(t, envVars, expectation) 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module env-aws-params 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.17.4 7 | github.com/aws/aws-sdk-go-v2/config v1.18.12 8 | github.com/aws/aws-sdk-go-v2/service/ssm v1.35.2 9 | github.com/sirupsen/logrus v1.9.0 10 | github.com/urfave/cli v1.22.12 11 | ) 12 | 13 | require ( 14 | github.com/aws/aws-sdk-go-v2/credentials v1.13.12 // indirect 15 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 // indirect 16 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.1 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 // indirect 23 | github.com/aws/smithy-go v1.13.5 // indirect 24 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 25 | github.com/jmespath/go-jmespath v0.4.0 // indirect 26 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 27 | golang.org/x/sys v0.1.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2 | github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY= 3 | github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 4 | github.com/aws/aws-sdk-go-v2/config v1.18.12 h1:fKs/I4wccmfrNRO9rdrbMO1NgLxct6H9rNMiPdBxHWw= 5 | github.com/aws/aws-sdk-go-v2/config v1.18.12/go.mod h1:J36fOhj1LQBr+O4hJCiT8FwVvieeoSGOtPuvhKlsNu8= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.13.12 h1:Cb+HhuEnV19zHRaYYVglwvdHGMJWbdsyP4oHhw04xws= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.13.12/go.mod h1:37HG2MBroXK3jXfxVGtbM2J48ra2+Ltu+tmwr/jO0KA= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 h1:3aMfcTmoXtTZnaT86QlVaYh+BRMbvrrmZwIQ5jWqCZQ= 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22/go.mod h1:YGSIJyQ6D6FjKMQh16hVFSIUD54L4F7zTGePqYMYYJU= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE= 11 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A= 13 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 h1:J4xhFd6zHhdF9jPP0FQJ6WknzBboGMBNjKOv4iTuw4A= 15 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29/go.mod h1:TwuqRBGzxjQJIwH16/fOZodwXt2Zxa9/cwJC5ke4j7s= 16 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22 h1:LjFQf8hFuMO22HkV5VWGLBvmCLBCLPivUAmpdpnp4Vs= 17 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22/go.mod h1:xt0Au8yPIwYXf/GYPy/vl4K3CgwhfQMYbrH7DlUUIws= 18 | github.com/aws/aws-sdk-go-v2/service/ssm v1.35.2 h1:PtV0g0sHaz8B4FD9M4zhdamFEoOYEo6O5nFv9LaWID8= 19 | github.com/aws/aws-sdk-go-v2/service/ssm v1.35.2/go.mod h1:VLSz2SHUKYFSOlXB/GlXoLU6KPYQJAbw7I20TDJdyws= 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.1 h1:lQKN/LNa3qqu2cDOQZybP7oL4nMGGiFqob0jZJaR8/4= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.1/go.mod h1:IgV8l3sj22nQDd5qcAGY0WenwCzCphqdbFOpfktZPrI= 22 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 h1:0bLhH6DRAqox+g0LatcjGKjjhU6Eudyys6HB6DJVPj8= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1/go.mod h1:O1YSOg3aekZibh2SngvCRRG+cRHKKlYgxf/JBF/Kr/k= 24 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 h1:s49mSnsBZEXjfGBkRfmK+nPqzT7Lt3+t2SmAKNyHblw= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.3/go.mod h1:b+psTJn33Q4qGoDaM7ZiOVVG8uVjGI6HaZ8WBHdgDgU= 26 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 27 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 28 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 29 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 30 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 34 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 36 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 37 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 38 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 42 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 43 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 44 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 47 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 48 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 51 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 52 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 53 | github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= 54 | github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= 55 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 57 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 59 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 60 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 61 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "github.com/urfave/cli" 10 | "strings" 11 | ) 12 | 13 | var VersionString string 14 | 15 | func init() { 16 | log.SetLevel(log.InfoLevel) 17 | } 18 | 19 | func main() { 20 | app := cli.NewApp() 21 | app.Name = "env-aws-params" 22 | app.Usage = "Application entry-point that injects SSM Parameter Store values as Environment Variables" 23 | app.UsageText = "env-aws-params [global options] -p prefix command [command arguments]" 24 | app.Version = VersionString 25 | app.Flags = cliFlags() 26 | app.Action = func(c *cli.Context) error { 27 | return action(c) 28 | } 29 | err := app.Run(os.Args) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | 35 | func action(c *cli.Context) error { 36 | 37 | if c.GlobalBool("debug") { 38 | log.SetLevel(log.DebugLevel) 39 | } 40 | if c.GlobalBool("silent") { 41 | log.SetOutput(ioutil.Discard) 42 | } else { 43 | log.SetOutput(os.Stdout) 44 | } 45 | 46 | code, err := validateArgs(c) 47 | if code > 0 { 48 | return cli.NewExitError(errorPrefix(err), code) 49 | } 50 | 51 | params, err := getParameters(c) 52 | if err != nil { 53 | return cli.NewExitError(errorPrefix(err), -1) 54 | } 55 | 56 | envVars := BuildEnvVars( 57 | params, 58 | c.GlobalBool("sanitize"), 59 | c.GlobalBool("strip"), 60 | c.GlobalBool("upcase")) 61 | 62 | for _, v := range envVars { 63 | log.Debugf("Setting %s", v) 64 | } 65 | 66 | if c.GlobalBool("pristine") == false { 67 | envVars = append(os.Environ(), envVars...) 68 | } 69 | 70 | err = RunCommand(c.Args()[0], c.Args()[1:], envVars) 71 | if err != nil { 72 | if cmdError, ok := err.(*CommandFailedError); ok { 73 | return cli.NewExitError(errorPrefix(err), cmdError.ExitCode) 74 | } 75 | return cli.NewExitError(errorPrefix(err), 128) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func cliFlags() []cli.Flag { 82 | return []cli.Flag{ 83 | cli.StringFlag{ 84 | Name: "aws-region", 85 | Usage: "The AWS region to use for the Parameter Store API", 86 | EnvVar: "AWS_REGION", 87 | }, 88 | cli.StringFlag{ 89 | Name: "profile", 90 | Usage: "Optional AWS profile to use for the Parameter Store API", 91 | EnvVar: "AWS_PROFILE", 92 | }, 93 | cli.StringSliceFlag{ 94 | Name: "prefix, p", 95 | Usage: "Key prefix that is used to retrieve the environment variables - supports multiple use", 96 | EnvVar: "PARAMS_PREFIX", 97 | }, 98 | cli.BoolFlag{ 99 | Name: "pristine", 100 | Usage: "Only use values retrieved from Parameter Store, do not inherit the existing environment variables", 101 | EnvVar: "PARAMS_PRISTINE", 102 | }, 103 | cli.BoolFlag{ 104 | Name: "sanitize", 105 | Usage: "Replace invalid characters in keys to underscores", 106 | EnvVar: "PARAMS_SANITIZE", 107 | }, 108 | cli.BoolFlag{ 109 | Name: "strip", 110 | Usage: "Strip invalid characters in keys", 111 | EnvVar: "PARAMS_STRIP", 112 | }, 113 | cli.BoolFlag{ 114 | Name: "upcase", 115 | Usage: "Force keys to uppercase", 116 | EnvVar: "PARAMS_UPCASE", 117 | }, 118 | cli.BoolFlag{ 119 | Name: "debug", 120 | Usage: "Log additional debugging information", 121 | EnvVar: "PARAMS_DEBUG", 122 | }, 123 | cli.BoolFlag{ 124 | Name: "silent", 125 | Usage: "Silence all logs", 126 | EnvVar: "PARAMS_SILENT", 127 | }, 128 | } 129 | } 130 | 131 | func errorPrefix(err error) string { 132 | return strings.Join([]string{"ERROR:", err.Error()}, " ") 133 | } 134 | 135 | func getParameters(c *cli.Context) (map[string]string, error) { 136 | values := make(map[string]string) 137 | 138 | client, err := NewSSMClient(c.GlobalString("aws-region"), c.GlobalString("profile")) 139 | if err != nil { 140 | return values, err 141 | } 142 | 143 | for _, path := range c.GlobalStringSlice("prefix") { 144 | params, err := client.GetParametersByPath(path) 145 | if err != nil { 146 | return values, err 147 | } 148 | for k, v := range params { 149 | values[k] = v 150 | } 151 | } 152 | return values, nil 153 | } 154 | 155 | func validateArgs(c *cli.Context) (int, error) { 156 | if len(c.GlobalStringSlice("prefix")) == 0 { 157 | return 1, errors.New("prefix is required") 158 | } 159 | 160 | if c.NArg() == 0 { 161 | return 2, errors.New("command not specified") 162 | } 163 | 164 | if c.GlobalBool("sanitize") == true && c.GlobalBool("strip") == true { 165 | return 3, errors.New("--sanitize and --strip are mutually exclusive behaviors") 166 | } 167 | 168 | return 0, nil 169 | } 170 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/urfave/cli" 10 | ) 11 | 12 | func NewContext(t *testing.T, testArgs []string) *cli.Context { 13 | app := cli.NewApp() 14 | app.Writer = ioutil.Discard 15 | app.Flags = cliFlags() 16 | set := flag.NewFlagSet("test", 0) 17 | for _, f := range app.Flags { 18 | f.Apply(set) 19 | } 20 | set.Parse(testArgs) 21 | return cli.NewContext(app, set, nil) 22 | } 23 | 24 | func TestMissingPrefix(t *testing.T) { 25 | var testArgs []string 26 | 27 | testArgs = []string{"--upcase"} 28 | 29 | code, err := validateArgs(NewContext(t, testArgs)) 30 | if code != 1 { 31 | t.Fatalf("expected code to be 1, got %v", code) 32 | } 33 | if err == nil { 34 | t.Fatalf("expected err to be set, got nil") 35 | } 36 | } 37 | 38 | func TestMissingCommand(t *testing.T) { 39 | var testArgs []string 40 | 41 | testArgs = []string{"--prefix", "/foo"} 42 | 43 | code, err := validateArgs(NewContext(t, testArgs)) 44 | if code != 2 { 45 | t.Fatalf("expected code to be 2, got %v", code) 46 | } 47 | if err == nil { 48 | t.Fatalf("expected err to be set, got nil") 49 | } 50 | } 51 | 52 | func TestMissingStripAndSanitize(t *testing.T) { 53 | var testArgs []string 54 | 55 | testArgs = []string{"--prefix", "/foo", "--strip", "--sanitize", "/bin/bash"} 56 | 57 | code, err := validateArgs(NewContext(t, testArgs)) 58 | if code != 3 { 59 | t.Fatalf("expected code to be 3, got %v", code) 60 | } 61 | if err == nil { 62 | t.Fatalf("expected err to be set, got nil") 63 | } 64 | } 65 | 66 | func TestValidCLIOptions(t *testing.T) { 67 | var testArgs []string 68 | 69 | testArgs = []string{"--prefix", "/foo", "--strip", "/bin/bash"} 70 | 71 | code, err := validateArgs(NewContext(t, testArgs)) 72 | if code != 0 { 73 | t.Fatalf("expected code to be 0, got %v", code) 74 | } 75 | if err != nil { 76 | t.Fatalf("expected err to be nil, got %v", code) 77 | } 78 | } 79 | 80 | func TestErrorPrefix(t *testing.T) { 81 | testError := errors.New("foo bar") 82 | result := errorPrefix(testError) 83 | expectation := "ERROR: foo bar" 84 | if result != expectation { 85 | t.Fatalf("expected \"%v\", got \"%v\"", result, expectation) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "strings" 8 | "syscall" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type CommandFailedError struct { 14 | ExitCode int 15 | } 16 | 17 | func (e *CommandFailedError) Error() string { 18 | return fmt.Sprintf("Command failed with exit code %d", e.ExitCode) 19 | } 20 | 21 | func RunCommand(command string, args []string, envVars []string) error { 22 | 23 | log.Infof("PID %v running %s %s", os.Getpid(), command, 24 | strings.Join(args[:], " ")) 25 | 26 | procAttr := new(os.ProcAttr) 27 | procAttr.Env = envVars 28 | procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr} 29 | 30 | // prefix args with the command, as per https://golang.org/pkg/os/#StartProcess 31 | // The argv slice will become os.Args in the new process, so it normally starts 32 | // with the program name. 33 | args = append([]string{command}, args...) 34 | proc, err := os.StartProcess(command, args, procAttr) 35 | if err != nil { 36 | return err 37 | } 38 | sigc := make(chan os.Signal, 1) 39 | signal.Notify(sigc, 40 | syscall.SIGHUP, 41 | syscall.SIGINT, 42 | syscall.SIGTERM, 43 | syscall.SIGQUIT) 44 | go func() { 45 | sigv := <-sigc 46 | switch sigv { 47 | case syscall.SIGHUP: 48 | err = syscall.Kill(-os.Getpid(), syscall.SIGHUP) 49 | case syscall.SIGINT: 50 | err = syscall.Kill(-os.Getpid(), syscall.SIGINT) 51 | case syscall.SIGTERM: 52 | err = syscall.Kill(-os.Getpid(), syscall.SIGTERM) 53 | case syscall.SIGQUIT: 54 | err = syscall.Kill(-os.Getpid(), syscall.SIGQUIT) 55 | default: 56 | err = syscall.Kill(-os.Getpid(), syscall.SIGTERM) 57 | } 58 | log.WithFields(log.Fields{ 59 | "err": err, 60 | "proc": proc, 61 | "pid": -proc.Pid, 62 | "signal": sigv}, 63 | ).Info("Caught signal, sent to child") 64 | }() 65 | procState, err := proc.Wait() 66 | if err != nil { 67 | return err 68 | } 69 | if procState.ExitCode() != 0 { 70 | return &CommandFailedError{procState.ExitCode()} 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /ssm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/service/ssm" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type SSMClient struct { 15 | client *ssm.Client 16 | } 17 | 18 | func NewSSMClient(region string, profile string) (*SSMClient, error) { 19 | 20 | var cfg aws.Config 21 | var err error 22 | 23 | ctx := context.TODO() 24 | if profile != "" { 25 | cfg, err = config.LoadDefaultConfig(ctx, 26 | config.WithSharedConfigProfile(profile), 27 | ) 28 | } else { 29 | cfg, err = config.LoadDefaultConfig(ctx) 30 | } 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if region != "" { 36 | cfg.Region = region 37 | } 38 | 39 | client := ssm.NewFromConfig(cfg) 40 | return &SSMClient{client}, nil 41 | } 42 | 43 | func (c *SSMClient) GetParametersByPath(path string) (map[string]string, error) { 44 | if strings.HasSuffix(path, "/") != true { 45 | path = fmt.Sprintf("%s/", path) 46 | } 47 | 48 | var nextToken *string 49 | parameters := make(map[string]string) 50 | 51 | for { 52 | params := &ssm.GetParametersByPathInput{ 53 | Path: aws.String(path), 54 | Recursive: aws.Bool(true), 55 | WithDecryption: aws.Bool(true), 56 | MaxResults: aws.Int32(10), 57 | NextToken: nextToken, 58 | } 59 | response, err := c.client.GetParametersByPath(context.TODO(), params) 60 | 61 | if err != nil { 62 | log.Errorf("Error Getting Parameters from SSM: %s", err) 63 | return nil, err 64 | } 65 | 66 | for _, p := range response.Parameters { 67 | parameters[strings.TrimPrefix(*p.Name, path)] = *p.Value 68 | } 69 | 70 | if response.NextToken == nil { 71 | break 72 | } 73 | nextToken = response.NextToken 74 | } 75 | return parameters, nil 76 | } 77 | --------------------------------------------------------------------------------