├── .dockerignore ├── .gitignore ├── modd.conf ├── main.go ├── .goreleaser.yml ├── Dockerfile ├── ssm ├── transformations │ ├── trim.go │ ├── trim_test.go │ ├── template_funcs.go │ └── actions.go ├── util.go ├── expand.go ├── expand_test.go └── parameters.go ├── .github └── workflows │ ├── build_test.yml │ └── release.yml ├── cmd ├── print.go ├── dotenv.go ├── run.go └── root.go ├── go.mod ├── README.md ├── LICENSE └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | Dockerfile.build 3 | build.sh 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | ssm-parent 3 | *.log 4 | dist/ 5 | vendor/ 6 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | **/*.go { 2 | prep: go build 3 | } 4 | **/*.go { 5 | prep: go test -v ./... 6 | } 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/springload/ssm-parent/cmd" 4 | 5 | var version = "dev" 6 | 7 | func main() { 8 | cmd.Execute(version) 9 | } 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | builds: 3 | - binary: ssm-parent 4 | env: 5 | - CGO_ENABLED=0 6 | goos: 7 | - darwin 8 | - linux 9 | goarch: 10 | - amd64 11 | - arm64 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine as build 2 | 3 | RUN apk update && apk add git 4 | 5 | WORKDIR /app 6 | 7 | ADD go.mod go.sum ./ 8 | 9 | RUN go mod download 10 | 11 | ADD ./ ./ 12 | ENV CGO_ENABLED=0 13 | RUN go build 14 | 15 | FROM alpine as runtime 16 | 17 | COPY --from=build /app/ssm-parent /usr/bin 18 | 19 | ENTRYPOINT ["/usr/bin/ssm-parent"] 20 | -------------------------------------------------------------------------------- /ssm/transformations/trim.go: -------------------------------------------------------------------------------- 1 | package transformations 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func TrimKeys(parameters map[string]string, trim string, startsWith string) { 8 | 9 | for key, value := range parameters { 10 | if strings.HasPrefix(key, startsWith) { 11 | parameters[strings.TrimPrefix(key, trim)] = value 12 | delete(parameters, key) 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ssm/util.go: -------------------------------------------------------------------------------- 1 | package ssm 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var env Env 8 | 9 | // difference returns the elements in a that aren't in b 10 | // the second argument is slice of string pointers to suit AWS SDK 11 | func stringSliceDifference(a, b []string) []string { 12 | mb := map[string]bool{} 13 | for _, x := range b { 14 | mb[x] = true 15 | } 16 | ab := []string{} 17 | for _, x := range a { 18 | if _, ok := mb[x]; !ok { 19 | ab = append(ab, x) 20 | } 21 | } 22 | return ab 23 | } 24 | 25 | // Env just adapts os.LookupEnv to this interface 26 | type Env struct{} 27 | 28 | // Get gets env var by the provided key 29 | func (e Env) Get(key string) (string, bool) { 30 | return os.LookupEnv(key) 31 | } 32 | -------------------------------------------------------------------------------- /ssm/transformations/trim_test.go: -------------------------------------------------------------------------------- 1 | package transformations 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTrimKeys(t *testing.T) { 8 | t.Setenv("ENVIRONMENT", "teststaging") 9 | 10 | parameters := map[string]string{ 11 | "_PREFIXED_PARAMETER": "prefixed_value", 12 | "_ANOTHER_PARAMETER": "another_value", 13 | } 14 | 15 | expecting := map[string]string{ 16 | "prefixed_value": "PREFIXED_PARAMETER", 17 | "another_value": "_ANOTHER_PARAMETER", 18 | } 19 | 20 | TrimKeys(parameters, "_", "_PREFIXED_") 21 | 22 | // Swap result keys and values to check against expectations 23 | result := make(map[string]string) 24 | for key, value := range parameters { 25 | result[value] = key 26 | } 27 | 28 | for val, expectedParam := range expecting { 29 | if result[val] != expectedParam { 30 | t.Errorf("'%s's key should be '%s', but got '%s'", val, expectedParam, result[val]) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build_test.yml: -------------------------------------------------------------------------------- 1 | name: "Test the build" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: test if ssm-parent can be built 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v3 14 | - name: set up Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: 1.24.x 18 | - name: cache modules 19 | uses: actions/cache@v4 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | 26 | - name: download dependencies 27 | run: go mod download 28 | 29 | - name: golangci-lint 30 | uses: golangci/golangci-lint-action@v3 31 | 32 | - name: build the app 33 | run: go build 34 | - name: test the app 35 | run: go test -v ./... 36 | -------------------------------------------------------------------------------- /cmd/print.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/springload/ssm-parent/ssm" 8 | 9 | "github.com/apex/log" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // printCmd represents the print command 15 | var printCmd = &cobra.Command{ 16 | Use: "print", 17 | Short: "Prints the specified parameters.", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | parameters, err := ssm.GetParameters( 20 | viper.GetStringSlice("names"), 21 | viper.GetStringSlice("paths"), 22 | viper.GetStringSlice("plain-names"), 23 | viper.GetStringSlice("plain-paths"), 24 | transformationsList, 25 | viper.GetBool("expand"), 26 | viper.GetBool("strict"), 27 | viper.GetBool("recursive"), 28 | viper.GetBool("expand-names"), 29 | viper.GetBool("expand-paths"), 30 | viper.GetStringSlice("expand-values"), 31 | ) 32 | if err != nil { 33 | log.WithError(err).Fatal("Can't marshal json") 34 | } 35 | marshalled, err := json.MarshalIndent(parameters, "", " ") 36 | if err != nil { 37 | log.WithError(err).Fatal("Can't marshal json") 38 | } 39 | fmt.Println(string(marshalled)) 40 | }, 41 | } 42 | 43 | func init() { 44 | rootCmd.AddCommand(printCmd) 45 | } 46 | -------------------------------------------------------------------------------- /ssm/expand.go: -------------------------------------------------------------------------------- 1 | package ssm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/buildkite/interpolate" 8 | ) 9 | 10 | // expandArgs expands arguments using env vars 11 | func ExpandArgs(args []string) []string { 12 | var expanded []string 13 | for _, arg := range args { 14 | arg = expandValue(arg) 15 | expanded = append(expanded, arg) 16 | } 17 | return expanded 18 | } 19 | 20 | // expandValue interpolates values using env vars 21 | func expandValue(val string) string { 22 | e, err := interpolate.Interpolate(env, val) 23 | if err == nil { 24 | return strings.TrimSpace(string(e)) 25 | } 26 | return val 27 | } 28 | 29 | // expandParameters expands values using shell-like syntax 30 | func expandParameters(parameters map[string]string, expand bool, expandValues []string) error { 31 | 32 | // if global expand is true then just it for all 33 | if expand { 34 | for key, value := range parameters { 35 | parameters[key] = expandValue(value) 36 | } 37 | // can return early as we've done the job 38 | return nil 39 | } 40 | // check if all values that we ask to expand present in the parameters 41 | // otherwise, it's a configuration error 42 | for _, val := range expandValues { 43 | if _, ok := parameters[val]; !ok { 44 | return fmt.Errorf("env var %s is present in the expand-values but doesn't exist in the environment", val) 45 | } else { 46 | // if the var is present we expand it 47 | parameters[val] = expandValue(parameters[val]) 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /cmd/dotenv.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/springload/ssm-parent/ssm" 7 | 8 | "github.com/apex/log" 9 | "github.com/joho/godotenv" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // dotenvCmd represents the dotenv command 15 | var dotenvCmd = &cobra.Command{ 16 | Use: "dotenv ", 17 | Short: "Writes dotenv file", 18 | Long: `Gathers parameters from SSM Parameter store, writes .env file and exits`, 19 | Args: cobra.ExactArgs(1), 20 | Run: func(cmd *cobra.Command, args []string) { 21 | parameters, err := ssm.GetParameters( 22 | viper.GetStringSlice("names"), 23 | viper.GetStringSlice("paths"), 24 | viper.GetStringSlice("plain-names"), 25 | viper.GetStringSlice("plain-paths"), 26 | transformationsList, 27 | viper.GetBool("expand"), 28 | viper.GetBool("strict"), 29 | viper.GetBool("recursive"), 30 | viper.GetBool("expand-names"), 31 | viper.GetBool("expand-paths"), 32 | viper.GetStringSlice("expand-values"), 33 | ) 34 | if err != nil { 35 | log.WithError(err).Fatal("Can't get parameters") 36 | } 37 | 38 | // we don't want to use godotenv as it creates files with too open permissions 39 | content, err := godotenv.Marshal(parameters) 40 | if err != nil { 41 | log.WithError(err).Fatal("Can't marshal the env to a string") 42 | } 43 | 44 | file, err := os.OpenFile(args[0], os.O_WRONLY|os.O_CREATE, 0600) 45 | if err != nil { 46 | log.WithError(err).Fatal("Can't create the file") 47 | } 48 | 49 | _, err = file.WriteString(content) 50 | if err != nil { 51 | log.WithError(err).Fatal("Can't write the dotenv file") 52 | } else { 53 | log.WithFields(log.Fields{"filename": args[0]}).Info("Wrote the .env file") 54 | } 55 | }, 56 | } 57 | 58 | func init() { 59 | rootCmd.AddCommand(dotenvCmd) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/springload/ssm-parent/ssm" 10 | 11 | "github.com/apex/log" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | // runCmd represents the run command 17 | var runCmd = &cobra.Command{ 18 | Use: "run command", 19 | Short: "Runs the specified command", 20 | Args: cobra.MinimumNArgs(1), 21 | Run: func(cobraCmd *cobra.Command, args []string) { 22 | var cmdArgs []string 23 | 24 | parameters, err := ssm.GetParameters( 25 | viper.GetStringSlice("names"), 26 | viper.GetStringSlice("paths"), 27 | viper.GetStringSlice("plain-names"), 28 | viper.GetStringSlice("plain-paths"), 29 | transformationsList, 30 | viper.GetBool("expand"), 31 | viper.GetBool("strict"), 32 | viper.GetBool("recursive"), 33 | viper.GetBool("expand-names"), 34 | viper.GetBool("expand-paths"), 35 | viper.GetStringSlice("expand-values"), 36 | ) 37 | if err != nil { 38 | log.WithError(err).Fatal("Can't get parameters") 39 | } 40 | for key, value := range parameters { 41 | os.Setenv(key, value) 42 | } 43 | 44 | command, err := exec.LookPath(args[0]) 45 | ctx := log.WithFields(log.Fields{"command": command}) 46 | if err != nil { 47 | ctx.WithError(err).Fatal("Cant find the command") 48 | } 49 | cmdArgs = append(cmdArgs, args[:1]...) 50 | 51 | c := make(chan os.Signal, 1) 52 | signal.Notify(c) 53 | if viper.GetBool("expand") { 54 | cmdArgs = append(cmdArgs, ssm.ExpandArgs(args[1:])...) 55 | } else { 56 | cmdArgs = append(cmdArgs, args[1:]...) 57 | } 58 | if err := syscall.Exec(command, cmdArgs, os.Environ()); err != nil { 59 | ctx.WithError(err).Fatal("Can't run the command") 60 | } 61 | }, 62 | } 63 | 64 | func init() { 65 | rootCmd.AddCommand(runCmd) 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/springload/ssm-parent 2 | 3 | require ( 4 | github.com/apex/log v1.9.0 5 | github.com/aws/aws-sdk-go v1.55.7 6 | github.com/buildkite/interpolate v0.1.5 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/imdario/mergo v0.3.16 9 | github.com/joho/godotenv v1.5.1 10 | github.com/mitchellh/mapstructure v1.5.0 11 | github.com/spf13/cobra v1.9.1 12 | github.com/spf13/viper v1.20.1 13 | github.com/stretchr/testify v1.10.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/fsnotify/fsnotify v1.9.0 // indirect 19 | github.com/go-viper/mapstructure/v2 v2.3.0 // indirect 20 | github.com/hashicorp/errwrap v1.1.0 // indirect 21 | github.com/hashicorp/hcl v1.0.0 // indirect 22 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 23 | github.com/jmespath/go-jmespath v0.4.0 // indirect 24 | github.com/magiconair/properties v1.8.10 // indirect 25 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 26 | github.com/pkg/errors v0.9.1 // indirect 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 28 | github.com/sagikazarmark/locafero v0.9.0 // indirect 29 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 30 | github.com/sourcegraph/conc v0.3.0 // indirect 31 | github.com/spf13/afero v1.14.0 // indirect 32 | github.com/spf13/cast v1.9.2 // indirect 33 | github.com/spf13/pflag v1.0.6 // indirect 34 | github.com/subosito/gotenv v1.6.0 // indirect 35 | go.uber.org/atomic v1.11.0 // indirect 36 | go.uber.org/multierr v1.11.0 // indirect 37 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect 38 | golang.org/x/sys v0.34.0 // indirect 39 | golang.org/x/text v0.27.0 // indirect 40 | gopkg.in/ini.v1 v1.67.0 // indirect 41 | gopkg.in/yaml.v2 v2.4.0 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | ) 44 | 45 | go 1.24 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish ssm-parent 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build_and_release: 13 | name: build and release ssm-parent 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v2 19 | - 20 | name: Unshallow to restore tags 21 | run: git fetch --prune --unshallow 22 | - 23 | name: Set up Go 24 | uses: actions/setup-go@v1 25 | with: 26 | go-version: 1.24.x 27 | - 28 | name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v1 30 | with: 31 | version: latest 32 | args: release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | build_and_push_docker_image: 37 | runs-on: ubuntu-latest 38 | permissions: 39 | contents: read 40 | packages: write 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v3 45 | 46 | - name: Set up QEMU 47 | uses: docker/setup-qemu-action@v2 48 | 49 | - name: Set up Docker Buildx 50 | uses: docker/setup-buildx-action@v2 51 | 52 | - name: Log in to the Container registry 53 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 54 | with: 55 | registry: ${{ env.REGISTRY }} 56 | username: ${{ github.actor }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Extract metadata (tags, labels) for Docker 60 | id: meta 61 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 62 | with: 63 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 64 | tags: | 65 | type=semver,pattern={{version}} 66 | type=semver,pattern={{major}}.{{minor}} 67 | 68 | - name: Build and push Docker image 69 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 70 | with: 71 | context: . 72 | push: true 73 | platforms: linux/amd64,linux/arm64 74 | tags: ${{ steps.meta.outputs.tags }} 75 | labels: ${{ steps.meta.outputs.labels }} 76 | -------------------------------------------------------------------------------- /ssm/transformations/template_funcs.go: -------------------------------------------------------------------------------- 1 | package transformations 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "strings" 8 | "text/template" 9 | ) 10 | 11 | var funcMap = template.FuncMap{ 12 | "env": GetEnv, 13 | "url_host": URLHost, 14 | "url_port": URLPort, 15 | "url_password": URLPassword, 16 | "url_path": URLPath, 17 | "url_scheme": URLScheme, 18 | "url_user": URLUser, 19 | "trim_prefix": strings.TrimPrefix, 20 | "replace": strings.Replace, 21 | } 22 | 23 | // GetEnv gets the environment variable 24 | func GetEnv(input string) (string, error) { 25 | val, ok := os.LookupEnv(input) 26 | if !ok { 27 | return "", fmt.Errorf("can't find %s in the environment variables", input) 28 | } 29 | return val, nil 30 | } 31 | 32 | // URLUser extracts user from the URL or returns "" if it's not set 33 | func URLUser(input string) (string, error) { 34 | u, err := url.Parse(input) 35 | if err != nil { 36 | return "", err 37 | } 38 | return u.User.Username(), nil 39 | } 40 | 41 | // URLPassword extracts password from the URL or returns "" if it's not set 42 | func URLPassword(input string) (string, error) { 43 | u, err := url.Parse(input) 44 | if err != nil { 45 | return "", err 46 | } 47 | p, ok := u.User.Password() 48 | if !ok { 49 | return "", nil 50 | } 51 | return p, nil 52 | } 53 | 54 | // URLScheme extracts password from the URL or returns "" if it's not set 55 | func URLScheme(input string) (string, error) { 56 | u, err := url.Parse(input) 57 | if err != nil { 58 | return "", err 59 | } 60 | return u.Scheme, nil 61 | } 62 | 63 | // URLHost extracts host from the URL or returns "" if it's not set. It also strips any port if there is any 64 | func URLHost(input string) (string, error) { 65 | u, err := url.Parse(input) 66 | if err != nil { 67 | return "", err 68 | } 69 | return u.Hostname(), nil 70 | } 71 | 72 | // URLHost extracts host from the URL or returns "" if it's not set 73 | func URLPort(input string) (string, error) { 74 | u, err := url.Parse(input) 75 | if err != nil { 76 | return "", err 77 | } 78 | return u.Port(), nil 79 | } 80 | 81 | // URLPath extracts path from the URL or returns "" if it's not set 82 | func URLPath(input string) (string, error) { 83 | u, err := url.Parse(input) 84 | if err != nil { 85 | return "", err 86 | } 87 | return u.Path, nil 88 | } 89 | -------------------------------------------------------------------------------- /ssm/transformations/actions.go: -------------------------------------------------------------------------------- 1 | package transformations 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/template" 7 | 8 | "github.com/apex/log" 9 | ) 10 | 11 | type Transformation interface { 12 | Transform(map[string]string) (map[string]string, error) 13 | } 14 | 15 | // DeleteTransformation takes an input map and removes the specified keys 16 | type DeleteTransformation struct { 17 | Action string 18 | Rule []string 19 | } 20 | 21 | func (t *DeleteTransformation) Transform(source map[string]string) (map[string]string, error) { 22 | for _, key := range t.Rule { 23 | delete(source, key) 24 | } 25 | 26 | return source, nil 27 | } 28 | 29 | // RenameTransformation renames the keys in the input map 30 | type RenameTransformation struct { 31 | Action string 32 | Rule map[string]string 33 | } 34 | 35 | func (t *RenameTransformation) Transform(source map[string]string) (map[string]string, error) { 36 | for key, newkey := range t.Rule { 37 | if _, ok := source[key]; ok { 38 | log.Debugf("Renaming '%s' to '%s'", key, newkey) 39 | source[newkey] = source[key] 40 | delete(source, key) 41 | } else { 42 | log.Warnf("Not renaming '%s' to '%s' as it isn't set", key, newkey) 43 | } 44 | } 45 | 46 | return source, nil 47 | } 48 | 49 | // TemlateTransformation renames the keys in the input map 50 | type TemplateTransformation struct { 51 | Action string 52 | Rule map[string]string 53 | } 54 | 55 | func (t *TemplateTransformation) Transform(source map[string]string) (map[string]string, error) { 56 | for key, value := range t.Rule { 57 | tmpl, err := template.New("value").Funcs(funcMap).Parse(value) 58 | if err != nil { 59 | return source, fmt.Errorf("can't template '%s': %s", key, err) 60 | } 61 | var renderedValue strings.Builder 62 | err = tmpl.Execute(&renderedValue, source) 63 | if err != nil { 64 | return source, fmt.Errorf("can't execute template '%s': %s", key, err) 65 | } 66 | // if all good, then assign the rendered value 67 | source[key] = renderedValue.String() 68 | } 69 | 70 | return source, nil 71 | } 72 | 73 | // TrimTransformation modifies the keys in the input map 74 | type TrimTransformation struct { 75 | Action string 76 | Rule map[string]string 77 | } 78 | 79 | func (t *TrimTransformation) Transform(source map[string]string) (map[string]string, error) { 80 | if _, found := t.Rule["trim"]; !found { 81 | return source, fmt.Errorf("\"trim\" rule not set") 82 | } 83 | if _, found := t.Rule["starts_with"]; !found { 84 | return source, fmt.Errorf("\"starts_with\" rule not set") 85 | } 86 | TrimKeys(source, t.Rule["trim"], t.Rule["starts_with"]) 87 | 88 | return source, nil 89 | } 90 | -------------------------------------------------------------------------------- /ssm/expand_test.go: -------------------------------------------------------------------------------- 1 | package ssm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestExpandParameters(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | parameters map[string]string 14 | expandAll bool 15 | expandValues []string 16 | envVars map[string]string 17 | expectedParams map[string]string 18 | expectError bool 19 | }{ 20 | { 21 | name: "expand specific variable", 22 | parameters: map[string]string{ 23 | "DATABASE_NAME": "DB_$ENVIRONMENT", 24 | "SOME_SECRET": "abc$abc", 25 | }, 26 | expandAll: false, 27 | expandValues: []string{"DATABASE_NAME"}, 28 | envVars: map[string]string{ 29 | "ENVIRONMENT": "teststaging", 30 | }, 31 | expectedParams: map[string]string{ 32 | "DATABASE_NAME": "DB_teststaging", 33 | "SOME_SECRET": "abc$abc", 34 | }, 35 | expectError: false, 36 | }, 37 | { 38 | name: "expand all variables", 39 | parameters: map[string]string{ 40 | "DATABASE_NAME": "DB_$ENVIRONMENT", 41 | "SOME_SECRET": "abc$abc", 42 | }, 43 | expandAll: true, 44 | expandValues: []string{}, 45 | envVars: map[string]string{ 46 | "ENVIRONMENT": "teststaging", 47 | "abc": "def", 48 | }, 49 | expectedParams: map[string]string{ 50 | "DATABASE_NAME": "DB_teststaging", 51 | "SOME_SECRET": "abcdef", 52 | }, 53 | expectError: false, 54 | }, 55 | { 56 | name: "missing variable in selective expansion", 57 | parameters: map[string]string{ 58 | "SOME_SECRET": "abc$abc", 59 | }, 60 | expandAll: false, 61 | expandValues: []string{"NONEXISTENT"}, 62 | envVars: map[string]string{}, 63 | expectedParams: map[string]string{ 64 | "SOME_SECRET": "abc$abc", 65 | }, 66 | expectError: true, 67 | }, 68 | { 69 | name: "empty parameters", 70 | parameters: map[string]string{}, 71 | expandAll: false, 72 | expandValues: []string{"TEST"}, 73 | envVars: map[string]string{}, 74 | expectedParams: map[string]string{}, 75 | expectError: true, 76 | }, 77 | { 78 | name: "complex variable expansion", 79 | parameters: map[string]string{ 80 | "COMPLEX": "${VAR1}_${VAR2}_${VAR3}", 81 | }, 82 | expandAll: true, 83 | expandValues: []string{}, 84 | envVars: map[string]string{ 85 | "VAR1": "value1", 86 | "VAR2": "value2", 87 | "VAR3": "value3", 88 | }, 89 | expectedParams: map[string]string{ 90 | "COMPLEX": "value1_value2_value3", 91 | }, 92 | expectError: false, 93 | }, 94 | } 95 | 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | // Set up environment variables 99 | for k, v := range tt.envVars { 100 | t.Setenv(k, v) 101 | } 102 | 103 | // Run the test 104 | err := expandParameters(tt.parameters, tt.expandAll, tt.expandValues) 105 | 106 | // Check error 107 | if tt.expectError { 108 | assert.Error(t, err) 109 | } else { 110 | require.NoError(t, err) 111 | } 112 | 113 | // Check parameters 114 | for k, v := range tt.expectedParams { 115 | assert.Equal(t, v, tt.parameters[k], "parameter %s should be expanded correctly", k) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func BenchmarkExpandParameters(b *testing.B) { 122 | parameters := map[string]string{ 123 | "DATABASE_NAME": "DB_$ENVIRONMENT", 124 | "SOME_SECRET": "abc$abc", 125 | "COMPLEX": "${VAR1}_${VAR2}_${VAR3}", 126 | } 127 | 128 | b.Run("selective expansion", func(b *testing.B) { 129 | for i := 0; i < b.N; i++ { 130 | _ = expandParameters(parameters, false, []string{"DATABASE_NAME"}) 131 | } 132 | }) 133 | 134 | b.Run("expand all", func(b *testing.B) { 135 | for i := 0; i < b.N; i++ { 136 | _ = expandParameters(parameters, true, []string{}) 137 | } 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/apex/log" 8 | "github.com/mitchellh/mapstructure" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | "github.com/springload/ssm-parent/ssm/transformations" 12 | ) 13 | 14 | var ( 15 | config string 16 | transformationsList []transformations.Transformation 17 | ) 18 | 19 | // rootCmd represents the base command when called without any subcommands 20 | var rootCmd = &cobra.Command{ 21 | Use: "ssm-parent", 22 | Short: "Docker entrypoint that get parameters from AWS SSM Parameter Store", 23 | Long: `SSM-Parent is a docker entrypoint. 24 | 25 | It gets specified parameters (possibly secret) from AWS SSM Parameter Store, 26 | then exports them to the underlying process. Or creates a .env file to be consumed by an application. 27 | 28 | It reads parameters in the following order: path->name->plain-path->plain-name. 29 | So that every rightmost parameter overrides the previous one. 30 | `, 31 | } 32 | 33 | // Execute is the entrypoint for cmd/ module 34 | func Execute(version string) { 35 | rootCmd.Version = version 36 | if err := rootCmd.Execute(); err != nil { 37 | fmt.Println(err) 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | func initSettings() { 43 | if config != "" { 44 | viper.SetConfigFile(config) 45 | if err := viper.ReadInConfig(); err == nil { 46 | log.Infof("Using config file: %s", viper.ConfigFileUsed()) 47 | } else { 48 | log.WithError(err).Fatal("Had some errors while parsing the config") 49 | } 50 | } 51 | // parse to an array first 52 | var transformationsInterfaceArray = []interface{}{} 53 | if err := viper.UnmarshalKey("transformations", &transformationsInterfaceArray); err != nil { 54 | log.WithError(err).Fatal("can't decode config") 55 | } 56 | // unmarshal to the tiny struct first to see what the action is 57 | for n, t := range transformationsInterfaceArray { 58 | var hint = struct{ Action string }{} 59 | 60 | if err := mapstructure.Decode(t, &hint); err != nil { 61 | log.WithError(err).Fatal("can't decode config") 62 | } 63 | switch hint.Action { 64 | 65 | case "delete": 66 | tr := new(transformations.DeleteTransformation) 67 | if err := mapstructure.Decode(t, tr); err != nil { 68 | log.WithFields(log.Fields{ 69 | "transformation_number": n, 70 | "transformation_action": hint.Action, 71 | }).WithError(err).Fatal("can't decode config") 72 | } 73 | transformationsList = append(transformationsList, tr) 74 | case "rename": 75 | tr := new(transformations.RenameTransformation) 76 | if err := mapstructure.Decode(t, tr); err != nil { 77 | log.WithFields(log.Fields{ 78 | "transformation_number": n, 79 | "transformation_action": hint.Action, 80 | }).WithError(err).Fatal("can't decode config") 81 | } 82 | transformationsList = append(transformationsList, tr) 83 | case "template": 84 | tr := new(transformations.TemplateTransformation) 85 | if err := mapstructure.Decode(t, tr); err != nil { 86 | log.WithFields(log.Fields{ 87 | "transformation_number": n, 88 | "transformation_action": hint.Action, 89 | }).WithError(err).Fatal("can't decode config") 90 | } 91 | transformationsList = append(transformationsList, tr) 92 | case "trim_name_prefix": 93 | tr := new(transformations.TrimTransformation) 94 | if err := mapstructure.Decode(t, tr); err != nil { 95 | log.WithFields(log.Fields{ 96 | "transformation_number": n, 97 | "transformation_action": hint.Action, 98 | }).WithError(err).Fatal("can't decode config") 99 | } 100 | transformationsList = append(transformationsList, tr) 101 | 102 | default: 103 | log.Warnf("Got unparsed action: %s", hint.Action) 104 | } 105 | } 106 | 107 | if viper.GetBool("debug") { 108 | log.SetLevel(log.DebugLevel) 109 | } 110 | } 111 | func init() { 112 | cobra.OnInitialize(initSettings) 113 | rootCmd.PersistentFlags().StringVarP(&config, "config", "c", "", "Path to the config file (optional). Allows to set transformations") 114 | rootCmd.PersistentFlags().BoolP("debug", "d", false, "Turn on debug logging") 115 | rootCmd.PersistentFlags().BoolP("expand", "e", false, "Expand all arguments and values using shell-style syntax") 116 | rootCmd.PersistentFlags().BoolP("expand-names", "", false, "Expand SSM names using shell-style syntax. The '--expand' does the same, but this flag is more selective") 117 | rootCmd.PersistentFlags().BoolP("expand-paths", "", false, "Expand SSM paths using shell-style syntax. The '--expand' does the same, but this flag is more selective") 118 | rootCmd.PersistentFlags().StringSliceP("expand-values", "", []string{}, "Expand SSM values using shell-style syntax. The '--expand' does the same, but this flag is more selective. Can be specified multiple times.") 119 | rootCmd.PersistentFlags().StringSliceP("path", "p", []string{}, "Path to a SSM parameter. Expects JSON in the value. Can be specified multiple times.") 120 | rootCmd.PersistentFlags().StringSliceP("name", "n", []string{}, "Name of the SSM parameter to retrieve. Expects JSON in the value. Can be specified multiple times.") 121 | rootCmd.PersistentFlags().StringSliceP("plain-path", "", []string{}, "Path to a SSM parameter. Expects actual parameter in the value. Can be specified multiple times.") 122 | rootCmd.PersistentFlags().StringSliceP("plain-name", "", []string{}, "Name of the SSM parameter to retrieve. Expects actual parameter in the value. Can be specified multiple times.") 123 | rootCmd.PersistentFlags().BoolP("recursive", "r", false, "Walk through the provided SSM paths recursively.") 124 | rootCmd.PersistentFlags().BoolP("strict", "s", false, "Strict mode. Fail if found less parameters than number of names.") 125 | 126 | for _, err := range []error{ 127 | viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")), 128 | viper.BindPFlag("expand", rootCmd.PersistentFlags().Lookup("expand")), 129 | viper.BindPFlag("expand-names", rootCmd.PersistentFlags().Lookup("expand-names")), 130 | viper.BindPFlag("expand-paths", rootCmd.PersistentFlags().Lookup("expand-paths")), 131 | viper.BindPFlag("expand-values", rootCmd.PersistentFlags().Lookup("expand-values")), 132 | viper.BindPFlag("paths", rootCmd.PersistentFlags().Lookup("path")), 133 | viper.BindPFlag("names", rootCmd.PersistentFlags().Lookup("name")), 134 | viper.BindPFlag("plain-paths", rootCmd.PersistentFlags().Lookup("plain-path")), 135 | viper.BindPFlag("plain-names", rootCmd.PersistentFlags().Lookup("plain-name")), 136 | viper.BindPFlag("recursive", rootCmd.PersistentFlags().Lookup("recursive")), 137 | viper.BindPFlag("strict", rootCmd.PersistentFlags().Lookup("strict")), 138 | } { 139 | if err != nil { 140 | log.WithError(err).Fatalf("can't bind flags") 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/springload/ssm-parent)](https://goreportcard.com/report/github.com/springload/ssm-parent) 2 | 3 | ## SSM Parent 4 | 5 | This is wrapper entrypoint for Docker to do one thing: fetch parameters from SSM Parameter store and expose them as environment variables to the underlying process. 6 | 7 | Please note, that it still requires a proper `init` process, for example the one embedded into Docker can be used with `docker run --init`. 8 | 9 | ``` 10 | SSM-Parent is a docker entrypoint. 11 | 12 | It gets specified parameters (possibly secret) from AWS SSM Parameter Store, 13 | then exports them to the underlying process. Or creates a .env file to be consumed by an application. 14 | 15 | It reads parameters in the following order: path->name->plain-path->plain-name. 16 | So that every rightmost parameter overrides the previous one. 17 | 18 | Usage: 19 | ssm-parent [command] 20 | 21 | Available Commands: 22 | dotenv Writes dotenv file 23 | help Help about any command 24 | print Prints the specified parameters. 25 | run Runs the specified command 26 | 27 | Flags: 28 | -c, --config string Path to the config file (optional). Allows to set transformations 29 | -d, --debug Turn on debug logging 30 | -e, --expand Expand arguments and values using shell-style syntax 31 | -h, --help help for ssm-parent 32 | -n, --name strings Name of the SSM parameter to retrieve. Expects JSON in the value. Can be specified multiple times. 33 | -p, --path strings Path to a SSM parameter. Expects JSON in the value. Can be specified multiple times. 34 | --plain-name strings Name of the SSM parameter to retrieve. Expects actual parameter in the value. Can be specified multiple times. 35 | --plain-path strings Path to a SSM parameter. Expects actual parameter in the value. Can be specified multiple times. 36 | -r, --recursive Walk through the provided SSM paths recursively. 37 | -s, --strict Strict mode. Fail if found less parameters than number of names. 38 | --version version for ssm-parent 39 | 40 | Use "ssm-parent [command] --help" for more information about a command. 41 | ``` 42 | 43 | The SSM parameter names or paths can be specified with `-p` or `-n` flags. In this case all parameters must be in JSON format, i.e.: 44 | 45 | ``` 46 | { 47 | "ENVIRONMENT": "production" 48 | } 49 | ``` 50 | 51 | If several parameters are specified, all JSON entities will be read and merged into one, overriding existing keys, i.e. 52 | 53 | Parameter one: 54 | 55 | ``` 56 | { 57 | "USERNAME": "myuser", 58 | "DATABASE": "production" 59 | } 60 | ``` 61 | 62 | Parameter two: 63 | 64 | ``` 65 | { 66 | "DATABASE": "test" 67 | } 68 | ``` 69 | 70 | The result will be merged as this: 71 | 72 | ``` 73 | { 74 | "USERNAME": "myuser", 75 | "DATABASE": "test" 76 | } 77 | ``` 78 | 79 | One can also specify `--plain-name` and `--plain-path` command line options to read _plain_ parameters that are not in JSON format. 80 | `ssm-parent` takes the value as is, and constructs a key name from the `basename parameter`, 81 | i.e. a SSM Parameter `/project/environment/myParameter` with value `supervalue` will be exported as `myParameter=supervalue`. 82 | 83 | ### How to use 84 | 85 | Determine the paths you want to read and try it out with `ssm-parent print` to see the resulting JSON output. 86 | Then use `ssm-parent run` or `ssm-parent dotenv`. 87 | 88 | ### Variables transformations 89 | 90 | To transform variables, a config file is needed due to the complex nature of it. `ssm-parent` supports all config formats supported by https://github.com/spf13/viper, i.e. `.toml`, `.yaml`, `.json`. 91 | 92 | All configuration entities can be specified in there rather than as flags. 93 | The supported transformations are: 94 | 95 | 1. rename - renames env vars 96 | 2. delete - deletes env vars 97 | 3. template - templates env vars 98 | 4. trim_name_prefix - removes a prefix from variable names 99 | 100 | Rename, template, trim_name_prefix transformations expect a dictionary rule. The delete transformation expects an array. 101 | Template transformation uses [Go templates](https://golang.org/pkg/text/template/), and the environment variables map is passed as `.`. 102 | 103 | There are the following extra functions available in templates: url_host, url_user, url_password, url_path, url_scheme and trim_prefix. The current list of the custom functions can be found here https://github.com/springload/ssm-parent/blob/master/ssm/transformations/template_funcs.go#L9 104 | 105 | trim_name_prefix will match any variables starting with `starts_with` and will remove the `trim` string from the start of the corresponding variable names. 106 | 107 | There is practically no limit on the number of transformations and they are applied in order from top to the bottom. 108 | 109 | Below there is an example that recursively gets parameters from `/$PROJECT/common/` and `/$PROJECT/$ENVIRONMENT` and constructs variables out of 110 | `DATABASE_URL` to be consumed by an PHP application. It also renames `AWS_BUCKET` to `AWS_S3_BUCKET`, removes `DATABASE_URL` and trims a leading underscore from any variable name that may start with `_PHP`. 111 | 112 | ```yaml 113 | recursive: true 114 | expand: true 115 | debug: true 116 | paths: ["/$PROJECT/common/", "/$PROJECT/$ENVIRONMENT"] 117 | 118 | transformations: 119 | - action: template 120 | rule: 121 | SS_DATABASE_SERVER: "{{ url_host .DATABASE_URL }}" 122 | SS_DATABASE_USERNAME: "{{ url_user .DATABASE_URL }}" 123 | SS_DATABASE_PASSWORD: "{{ url_password .DATABASE_URL }}" 124 | SS_DATABASE_NAME: '{{ with $x := url_path .DATABASE_URL }}{{ trim_prefix $x "/" }}{{end}}' 125 | - action: rename 126 | rule: 127 | AWS_BUCKET: AWS_S3_BUCKET 128 | - action: delete 129 | rule: 130 | - DATABASE_URL 131 | - action: trim_name_prefix 132 | rule: 133 | trim: "_" 134 | starts_with: "_PHP" 135 | ``` 136 | 137 | ### Example Dockerfile part 138 | 139 | ``` 140 | ENV PROJECT myproject 141 | ENV ENVIRONMENT production 142 | 143 | RUN wget -O /tmp/ssm-parent.tar.gz https://github.com/springload/ssm-parent/releases/download/v1.4.1/ssm-parent_1.4.1_linux_amd64.tar.gz && \ 144 | tar xvf /tmp/ssm-parent.tar.gz && mv ssm-parent /usr/bin/ssm-parent && rm /tmp/ssm-parent.tar.gz 145 | 146 | ENTRYPOINT ["/usr/bin/ssm-parent", "run", "-e", "-p", "/$PROJECT/$ENVIRONMENT/backend/", "-r", "--"] 147 | CMD ["caddy" , "--conf", "/etc/Caddyfile", "--log", "stdout"] 148 | ``` 149 | 150 | ### Use as a Docker stage 151 | 152 | ``` 153 | # get the ssm-parent as a Docker stage 154 | FROM springload/ssm-parent:1.4.1 as ssm-parent 155 | 156 | # your main stage 157 | FROM alpine 158 | ENV PROJECT myproject 159 | ENV ENVIRONMENT production 160 | 161 | COPY --from=ssm-parent /usr/bin/ssm-parent /usr/bin/ssm-parent 162 | 163 | ENTRYPOINT ["/usr/bin/ssm-parent", "run", "-e", "-p", "/$PROJECT/$ENVIRONMENT/backend/", "-r", "--"] 164 | CMD ["caddy" , "--conf", "/etc/Caddyfile", "--log", "stdout"] 165 | ``` 166 | 167 | ### Config generation 168 | 169 | If your application can't be configured via environment variables, then the following script, utilising `envsubst`, can be used to generate configs. 170 | 171 | ``` 172 | #!/bin/sh 173 | 174 | echo "Bootstrapping Caddy" 175 | envsubst < /etc/Caddyfile.env > /etc/Caddyfile 176 | 177 | exec $@ 178 | ``` 179 | 180 | ### .env file generation 181 | 182 | Sometimes you just want a .env file, and it is also possible. 183 | 184 | Just specify all the same parameters, but use `dotenv` command instead with a filename to generate `.env` file. 185 | 186 | ``` 187 | ./ssm-parent dotenv -r -p /project/environment dotenv.env 188 | 2018/10/01 16:37:59 info Wrote the .env file filename=dotenv.env 189 | ``` 190 | 191 | ### How to build 192 | 193 | This project uses `go mod` as a dependency manager. Go v.1.13 was used. 194 | 195 | ``` 196 | $git clone https://github.com/springload/ssm-parent.git 197 | $go build 198 | # (after some hacking) 199 | $git tag vXXX && git push && git push --tags 200 | $goreleaser # to create a new release 201 | ``` 202 | -------------------------------------------------------------------------------- /ssm/parameters.go: -------------------------------------------------------------------------------- 1 | package ssm 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | goPath "path" 7 | "strings" 8 | 9 | "github.com/apex/log" 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/ssm" 13 | multierror "github.com/hashicorp/go-multierror" 14 | "github.com/imdario/mergo" 15 | 16 | "github.com/springload/ssm-parent/ssm/transformations" 17 | ) 18 | 19 | var localSession *session.Session 20 | 21 | func makeSession() error { 22 | if localSession == nil { 23 | log.Debug("Creating session") 24 | var err error 25 | // create AWS session 26 | localSession, err = session.NewSessionWithOptions(session.Options{ 27 | Config: aws.Config{}, 28 | SharedConfigState: session.SharedConfigEnable, 29 | Profile: "", 30 | }) 31 | if err != nil { 32 | return fmt.Errorf("can't get aws session") 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | func collectJsonParameters(responseParameters []*ssm.Parameter) (parameters []map[string]string, errors []error) { 39 | for _, parameter := range responseParameters { 40 | value := make(map[string]string) 41 | if innerErr := json.Unmarshal([]byte(aws.StringValue(parameter.Value)), &value); innerErr != nil { 42 | errors = append(errors, fmt.Errorf("Can't unmarshal json from '%s': %s", aws.StringValue(parameter.Name), innerErr)) 43 | } else { 44 | parameters = append(parameters, value) 45 | } 46 | } 47 | return 48 | } 49 | 50 | func getJsonSSMParametersByPaths(paths []string, strict, recursive bool) (parameters []map[string]string, err error) { 51 | err = makeSession() 52 | if err != nil { 53 | log.WithError(err).Fatal("Can't create session") // fail early here 54 | } 55 | s := ssm.New(localSession) 56 | for _, path := range paths { 57 | innerErr := s.GetParametersByPathPages(&ssm.GetParametersByPathInput{ 58 | Path: aws.String(path), 59 | WithDecryption: aws.Bool(true), 60 | Recursive: aws.Bool(recursive), 61 | }, func(response *ssm.GetParametersByPathOutput, last bool) bool { 62 | innerParameters, errs := collectJsonParameters(response.Parameters) 63 | for _, parseErr := range errs { 64 | err = multierror.Append(err, parseErr) 65 | } 66 | parameters = append(parameters, innerParameters...) 67 | 68 | return true 69 | }, 70 | ) 71 | if innerErr != nil { 72 | err = multierror.Append(err, fmt.Errorf("Can't get parameters from path '%s': %s", path, innerErr)) 73 | } 74 | } 75 | 76 | return 77 | } 78 | 79 | func getJsonSSMParameters(names []string, strict bool) (parameters []map[string]string, err error) { 80 | err = makeSession() 81 | if err != nil { 82 | log.WithError(err).Fatal("Can't create session") // fail early here 83 | } 84 | s := ssm.New(localSession) 85 | response, err := s.GetParameters(&ssm.GetParametersInput{ 86 | Names: aws.StringSlice(names), 87 | WithDecryption: aws.Bool(true), 88 | }) 89 | if err != nil { 90 | return nil, err 91 | } 92 | if len(response.Parameters) < len(names) { 93 | if strict { 94 | err = multierror.Append(err, fmt.Errorf("Found %d parameters from %d names", len(response.Parameters), len(names))) 95 | } else { 96 | var found []string 97 | for _, f := range response.Parameters { 98 | found = append(found, aws.StringValue(f.Name)) 99 | } 100 | diff := stringSliceDifference(names, found) 101 | log.WithFields(log.Fields{"missing_names": diff}).Warn("Some parameters have not been found") 102 | } 103 | } 104 | innerParameters, errs := collectJsonParameters(response.Parameters) 105 | for _, parseErr := range errs { 106 | err = multierror.Append(err, parseErr) 107 | } 108 | parameters = append(parameters, innerParameters...) 109 | return 110 | } 111 | 112 | func collectPlainParameters(responseParameters []*ssm.Parameter) (parameters []map[string]string, errors []error) { 113 | for _, parameter := range responseParameters { 114 | values := make(map[string]string) 115 | values[goPath.Base(aws.StringValue(parameter.Name))] = aws.StringValue(parameter.Value) 116 | parameters = append(parameters, values) 117 | } 118 | return 119 | } 120 | 121 | func getPlainSSMParametersByPaths(paths []string, strict, recursive bool) (parameters []map[string]string, err error) { 122 | err = makeSession() 123 | if err != nil { 124 | log.WithError(err).Fatal("Can't create session") // fail early here 125 | } 126 | s := ssm.New(localSession) 127 | for _, path := range paths { 128 | innerErr := s.GetParametersByPathPages(&ssm.GetParametersByPathInput{ 129 | Path: aws.String(path), 130 | WithDecryption: aws.Bool(true), 131 | Recursive: aws.Bool(recursive), 132 | }, func(response *ssm.GetParametersByPathOutput, last bool) bool { 133 | innerParameters, errs := collectPlainParameters(response.Parameters) 134 | for _, parseErr := range errs { 135 | err = multierror.Append(err, parseErr) 136 | } 137 | parameters = append(parameters, innerParameters...) 138 | 139 | return true 140 | }, 141 | ) 142 | if innerErr != nil { 143 | err = multierror.Append(err, fmt.Errorf("Can't get parameters from path '%s': %s", path, innerErr)) 144 | } 145 | } 146 | return 147 | } 148 | 149 | func getPlainSSMParameters(names []string, strict bool) (parameters []map[string]string, err error) { 150 | err = makeSession() 151 | if err != nil { 152 | log.WithError(err).Fatal("Can't create session") // fail early here 153 | } 154 | s := ssm.New(localSession) 155 | response, err := s.GetParameters(&ssm.GetParametersInput{ 156 | Names: aws.StringSlice(names), 157 | WithDecryption: aws.Bool(true), 158 | }) 159 | if err != nil { 160 | return nil, err 161 | } 162 | if len(response.Parameters) < len(names) { 163 | if strict { 164 | err = multierror.Append(err, fmt.Errorf("Found %d parameters from %d names", len(response.Parameters), len(names))) 165 | } else { 166 | var found []string 167 | for _, f := range response.Parameters { 168 | found = append(found, aws.StringValue(f.Name)) 169 | } 170 | diff := stringSliceDifference(names, found) 171 | log.WithFields(log.Fields{"missing_names": diff}).Warn("Some parameters have not been found") 172 | } 173 | } 174 | innerParameters, errs := collectPlainParameters(response.Parameters) 175 | for _, parseErr := range errs { 176 | err = multierror.Append(err, parseErr) 177 | } 178 | parameters = append(parameters, innerParameters...) 179 | return 180 | } 181 | 182 | // GetParameters returns all parameters by path/names, with optional env vars expansion 183 | func getAllParameters(names, paths, plainNames, plainPaths []string, strict, recursive bool) (parameters []map[string]string, err error) { 184 | if len(paths) > 0 { 185 | parametersFromPaths, err := getJsonSSMParametersByPaths(paths, strict, recursive) 186 | if err != nil { 187 | log.WithError(err).WithFields( 188 | log.Fields{"paths": paths}, 189 | ).Fatal("Can't get parameters by paths") 190 | } 191 | parameters = append(parameters, parametersFromPaths...) 192 | } 193 | if len(names) > 0 { 194 | parametersFromNames, err := getJsonSSMParameters(names, strict) 195 | if err != nil { 196 | log.WithError(err).WithFields( 197 | log.Fields{"names": names}, 198 | ).Fatal("Can't get parameters by names") 199 | } 200 | parameters = append(parameters, parametersFromNames...) 201 | } 202 | if len(plainPaths) > 0 { 203 | parametersFromPlainPaths, err := getPlainSSMParametersByPaths(plainPaths, strict, recursive) 204 | if err != nil { 205 | log.WithError(err).WithFields( 206 | log.Fields{"plain_paths": plainPaths}, 207 | ).Fatal("Can't get plain parameters by paths") 208 | } 209 | parameters = append(parameters, parametersFromPlainPaths...) 210 | } 211 | 212 | if len(plainNames) > 0 { 213 | parametersFromPlainNames, err := getPlainSSMParameters(plainNames, strict) 214 | if err != nil { 215 | log.WithError(err).WithFields( 216 | log.Fields{"plain_names": plainNames}, 217 | ).Fatal("Can't get plain parameters by names") 218 | } 219 | parameters = append(parameters, parametersFromPlainNames...) 220 | } 221 | 222 | return 223 | } 224 | 225 | // GetParameters returns all parameters by path/names, with optional env vars expansion 226 | func GetParameters(names, paths, plainNames, plainPaths []string, transformationsList []transformations.Transformation, expand, strict, recursive, expandNames, expandPaths bool, expandValues []string) (parameters map[string]string, err error) { 227 | localNames := names 228 | localPaths := paths 229 | localPlainNames := plainNames 230 | localPlainPaths := plainPaths 231 | 232 | if expand || expandNames { 233 | localNames = ExpandArgs(names) 234 | localPlainNames = ExpandArgs(plainNames) 235 | } 236 | if expand || expandPaths { 237 | localPaths = ExpandArgs(paths) 238 | localPlainPaths = ExpandArgs(plainPaths) 239 | } 240 | 241 | allParameters, err := getAllParameters(localNames, localPaths, localPlainNames, localPlainPaths, strict, recursive) 242 | if err != nil { 243 | return parameters, err 244 | } 245 | 246 | parameters = make(map[string]string) 247 | for _, parameter := range allParameters { 248 | // Normalize keys to uppercase before merging to solve Viper case sensitivity issue 249 | normalized := make(map[string]string, len(parameter)) 250 | for k, v := range parameter { 251 | normalized[strings.ToUpper(k)] = v 252 | } 253 | err = mergo.Merge(¶meters, &normalized, mergo.WithOverride) 254 | if err != nil { 255 | log.WithError(err).Fatal("Can't merge maps") 256 | } 257 | } 258 | 259 | if err := expandParameters(parameters, expand, expandValues); err != nil { 260 | log.WithError(err).Fatal("Can't expand vars") 261 | } 262 | 263 | // Normalize transformation rule keys to uppercase to match parameter keys 264 | for _, transformation := range transformationsList { 265 | switch t := transformation.(type) { 266 | case *transformations.RenameTransformation: 267 | normalizedRule := make(map[string]string, len(t.Rule)) 268 | for key, value := range t.Rule { 269 | normalizedRule[strings.ToUpper(key)] = value 270 | } 271 | t.Rule = normalizedRule 272 | case *transformations.TemplateTransformation: 273 | normalizedRule := make(map[string]string, len(t.Rule)) 274 | for key, value := range t.Rule { 275 | normalizedRule[strings.ToUpper(key)] = value 276 | } 277 | t.Rule = normalizedRule 278 | case *transformations.DeleteTransformation: 279 | for i, key := range t.Rule { 280 | t.Rule[i] = strings.ToUpper(key) 281 | } 282 | } 283 | // Note: TrimTransformation config keys (trim, starts_with) are NOT normalized 284 | // because they are configuration instructions, not parameter names 285 | } 286 | 287 | for _, transformation := range transformationsList { 288 | parameters, err = transformation.Transform(parameters) 289 | if err != nil { 290 | log.WithError(err).Fatal("can't transform parameter") 291 | } 292 | } 293 | return 294 | } 295 | 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= 2 | github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= 3 | github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= 4 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 5 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 6 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 7 | github.com/aws/aws-sdk-go v1.50.35 h1:llQnNddBI/64pK7pwUFBoWYmg8+XGQUCs214eMbSDZc= 8 | github.com/aws/aws-sdk-go v1.50.35/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= 9 | github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= 10 | github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 11 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 12 | github.com/buildkite/interpolate v0.0.0-20180215132703-c1c376f870d2 h1:v+yPQjl8QlVDFwU/z2jGM2G/Ievlck0ZCyDMVe07Xro= 13 | github.com/buildkite/interpolate v0.0.0-20180215132703-c1c376f870d2/go.mod h1:gbPR1gPu9dB96mucYIR7T3B7p/78hRVSOuzIWLHK2Y4= 14 | github.com/buildkite/interpolate v0.1.5 h1:v2Ji3voik69UZlbfoqzx+qfcsOKLA61nHdU79VV+tPU= 15 | github.com/buildkite/interpolate v0.1.5/go.mod h1:dHnrwHew5O8VNOAgMDpwRlFnhL5VSN6M1bHVmRZ9Ccc= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 23 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 24 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 25 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 26 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 27 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 28 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 29 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 30 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 31 | github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= 32 | github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 36 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 37 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 38 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 39 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 40 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 41 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 42 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 43 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 44 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 45 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 46 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 47 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 48 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 49 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 50 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 51 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 52 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 53 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 54 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 55 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 56 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 57 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 58 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 59 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 60 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 61 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 62 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 63 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 64 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 65 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 66 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 67 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 68 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 69 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 70 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 71 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 72 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 73 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 74 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 75 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 76 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 77 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 78 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 79 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 80 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 81 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 82 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 83 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 84 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 85 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 86 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 88 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 89 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 91 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 92 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 93 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 94 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 95 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 96 | github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= 97 | github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 98 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 99 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 100 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 101 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 102 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 103 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 104 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 105 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 106 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 107 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 108 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 109 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 110 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 111 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 112 | github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= 113 | github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 114 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 115 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 116 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 117 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 118 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 119 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 120 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 121 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 122 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 123 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 124 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 125 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 126 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 127 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 128 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 129 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 130 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 131 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 132 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 133 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 134 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 135 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 136 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 137 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 138 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 139 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 140 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= 141 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 142 | github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= 143 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 144 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 145 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 146 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 147 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 148 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 149 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 150 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 151 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 152 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 153 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 154 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 155 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 156 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 157 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 158 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 159 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 160 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 162 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 163 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 165 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 167 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 169 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 170 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 171 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 172 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 173 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 174 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 175 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 176 | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= 177 | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 178 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 179 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 181 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 182 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 183 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 184 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 185 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 186 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 187 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 188 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 189 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 190 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 191 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 192 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 193 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 194 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 195 | --------------------------------------------------------------------------------