├── .github └── workflows │ └── tag.yml ├── .gitignore ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── cmd ├── mlock-linux.go ├── mlock-non-linux.go └── root.go ├── cram_tests ├── inject.t ├── regress.t ├── run_option.t └── setup.sh ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── no_secrets.yml ├── reader ├── reader.go └── reader_test.go └── variables.yml /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag, Release and Upload 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | Release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 12 | - name: Bump version and push tag 13 | id: tag_release 14 | uses: anothrNick/github-tag-action@1.67.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | DEFAULT_BUMP: patch 18 | - name: Create Release 19 | id: create_release 20 | uses: actions/create-release@latest 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ steps.tag_release.outputs.tag }} 25 | release_name: Release ${{ steps.tag_release.outputs.tag }} 26 | body: GitHub Actions Release 27 | draft: false 28 | prerelease: false 29 | - name: Set up Go 1.21 30 | uses: actions/setup-go@v1 31 | with: 32 | go-version: 1.21 33 | id: go 34 | - name: Set up Python 3.13 35 | uses: actions/setup-python@v3 36 | with: 37 | python-version: "3.13" 38 | - name: Install cram 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install cram 42 | - name: Check out new tag into the Go module directory 43 | uses: actions/checkout@v2 44 | with: 45 | ref: ${{ steps.tag_release.outputs.tag }} 46 | - name: Deps & Build 47 | shell: bash 48 | run: | 49 | export PATH=${PATH}:`go env GOPATH`/bin 50 | make build-deps 51 | make build 52 | - name: Upload binaries to release 53 | uses: svenstaro/upload-release-action@v1-release 54 | with: 55 | repo_token: ${{ secrets.GITHUB_TOKEN }} 56 | file: buildenv-* 57 | tag: ${{ steps.tag_release.outputs.tag }} 58 | overwrite: true 59 | file_glob: true 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | buildenv 2 | pkg/ 3 | buildenv-*.tar.gz -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## CLA 2 | 3 | - If you would like to contribute code to this project you can do so through GitHub by forking the repository and sending a pull request. 4 | - Before Comcast merges your code into the project you must sign the [Comcast Contributor License Agreement (CLA)]( https://gist.github.com/ComcastOSS/a7b8933dd8e368535378cda25c92d19a). 5 | - If you haven't previously signed a Comcast CLA, you'll automatically be asked to when you open a pull request. Alternatively, we can send you a PDF that you can sign and scan back to us. Please create a new GitHub issue to request a PDF version of the CLA. 6 | 7 | ## Developer 8 | 9 | - Fork the codebase on github 10 | - Clone your fork to your machine 11 | - Create a branch describing your change 12 | - Develop your feature and push changes to your branch in your fork 13 | - Add your name to the `CONTRIBUTORS.md` file 14 | - Open a pull request from your fork and branch to master on [https://github.com/Comcast/Buildenv-Tool](https://github.com/Comcast/Buildenv-Tool) 15 | 16 | 17 | ## General Requirements 18 | 19 | - The code must be formatted with `go fmt` 20 | - The change must pass all tests 21 | - The change must include tests for new functionality created 22 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | - [Lew Goettner](mailto:Lewis_Goettner@comcast.com) 2 | - [Tyler Rivera](mailto:Tyler_Rivera@comcast.com) 3 | - [Ryan Eskin](mailto:Ryan_Eskin@comcast.com) 4 | - [Peter Shrom](mailto:Peter_Shrom@comcast.com) 5 | - [Terrance Miller](mailto:Terrance_Miller2@comcast.com) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git describe --tags `git rev-list --tags --max-count=1`) 2 | PROJECT_NAME := buildenv 3 | 4 | .phony: all build-deps build clean 5 | 6 | all: clean build-deps build 7 | 8 | test: build-local 9 | go test ./... 10 | cram cram_tests 11 | 12 | build-deps: 13 | go install github.com/mitchellh/gox@latest 14 | 15 | build: test 16 | CGO_ENABLED=0 gox -ldflags "-X main.version=$(VERSION)" -osarch="darwin/amd64 darwin/arm64 linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64" -output "pkg/{{.OS}}_{{.Arch}}/$(PROJECT_NAME)" 17 | for pkg in $$(ls pkg/); do cp CONTRIBUTING.md CONTRIBUTORS.md LICENSE NOTICE pkg/$${pkg}; done 18 | for pkg in $$(ls pkg/); do cd pkg/$${pkg}; tar cvzf "../../$(PROJECT_NAME)-$${pkg}-$(VERSION).tar.gz" *; cd ../..; done 19 | 20 | build-local: 21 | CGO_ENABLED=0 go build -ldflags "-X main.version=$(VERSION)" -o $(PROJECT_NAME) 22 | 23 | clean: 24 | rm -rf buildenv 25 | rm -f *.tar.gz 26 | rm -rf pkg 27 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Comcast Cable Communications Management, LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | This product includes software developed at Comcast (http://www.comcast.com/). 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | buildenv 2 | ======== 3 | 4 | A tool for generating environment exports from a YAML file. Variables can be set in plain test, or by specifying vault key-value (version 2) paths and keys (`kv_secrets`) or the older generic / kv paths (`secrets`) where the key name "value" is assumed. Buildenv will autodetect between version 2 and version 1 `kv_secret` paths _unless it can't read the mount details_. For that case, `kv_secrets` will assume version 2, and `kv1_secrets` will use version 1. 5 | 6 | Usage 7 | ----- 8 | 9 | Given a `variables.yml` file like this: 10 | ```yaml 11 | --- 12 | vars: 13 | GLOBAL: "global" 14 | 15 | secrets: 16 | GENERIC_SECRET: "gen/test" 17 | KV_SECRET: "old/test" 18 | KV2_SECRET: "secret/oldstyle" 19 | 20 | kv_secrets: 21 | - path: "secret/test" 22 | vars: 23 | KV2_ONE: "one" 24 | KV2_TWO: "two" 25 | - path: "old/test" 26 | vars: 27 | KV1: "value" 28 | - path: "gen/test" 29 | vars: 30 | KV_GENERIC: "value" 31 | 32 | kv1_secrets: 33 | - path: "old/test" 34 | vars: 35 | KV1SPECIFIC: "value" 36 | 37 | environments: 38 | stage: 39 | vars: 40 | ENVIRONMENT: "stage" 41 | 42 | secrets: 43 | ANOTHER_SECRET: "secret/oldstyle" 44 | 45 | dcs: 46 | ndc_one: 47 | vars: 48 | DC: "one" 49 | kv_secrets: 50 | - path: "old/test" 51 | vars: 52 | KV2_THREE: "three" 53 | ``` 54 | 55 | Output would look like this: 56 | 57 | ```bash 58 | % buildenv -c -e stage -d ndc_one 59 | # Global Variables 60 | export GLOBAL="global" 61 | export KV2_ONE="1" # Path: secret/test, Key: one 62 | export KV2_TWO="2" # Path: secret/test, Key: two 63 | export KV1="old" # Path: old/test, Key: value 64 | export KV_GENERIC="generic" # Path: gen/test, Key: value 65 | export KV1SPECIFIC="old" # Path: old/test, Key: value 66 | export GENERIC_SECRET="generic" # Path: gen/test, Key: value 67 | export KV_SECRET="old" # Path: old/test, Key: value 68 | export KV2_SECRET="default" # Path: secret/oldstyle, Key: value 69 | # Environment: stage 70 | export ENVIRONMENT="stage" 71 | export ANOTHER_SECRET="default" # Path: secret/oldstyle, Key: value 72 | # Datacenter: ndc_one 73 | export DC="one" 74 | export KV2_THREE="3" # Path: old/test, Key: three 75 | ``` 76 | 77 | Another mode uses -r to run a command. All exports will be provided directly to a subshell invoked with the command. This is especially useful in the context of a Makefile where it's very awkward to export lists of environment variables. An added benefit is it's now trivial to set environment variables just for a single command without causing any side-effects for subsequent commands. 78 | 79 | Example Makefile: 80 | 81 | ``` 82 | list-buckets: creds.yml 83 | buildenv -e stage -f $< -r "aws s3 ls" 84 | ``` 85 | 86 | *A Note About Vault:* If you have `secrets` or `kv_secrets` defined in either the global or environment scope, it's a mapping from environment variable to the path & key in vault. Buildenv uses all the standard vault environment variables to communicate with vault (`VAULT_ADDR` and `VAULT_TOKEN` being the two you're most likely to use.) You can find the complete list [in the vault client docs](https://pkg.go.dev/github.com/hashicorp/vault-client-go@v0.4.2#WithEnvironment). 87 | 88 | 89 | Running on Linux or in Docker container 90 | ---------- 91 | 92 | It is recommended to use the flag `-m` when running on linux or docker container with swap enabled. This will attempt to lock memory and prevent secrets from being written to swap space. If running on a docker container it may be necessary to add `--cap-add=IPC_LOCK` to the `docker run` command or in the `docker-compose` file to allow this. More info can be found at https://hub.docker.com/_/vault under Memory Locking and 'setcap'. 93 | 94 | Developing 95 | ---------- 96 | 97 | To test with vault, run: 98 | 99 | ```bash 100 | docker-compose up vault -d 101 | export VAULT_ADDR="http://localhost:8200" 102 | export VAULT_TOKEN="test" 103 | vault secrets enable -path gen generic 104 | vault secrets enable -version=1 -path old kv 105 | vault kv put secret/test "one=1" "two=2" 106 | vault kv put secret/oldstyle "value=default" 107 | vault kv put old/test "value=old" "three=3" 108 | vault write gen/test "value=generic" 109 | 110 | buildenv -c -e stage -d ndc_one 111 | docker-compose down 112 | ``` 113 | -------------------------------------------------------------------------------- /cmd/mlock-linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "syscall" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func EnableMlock() error { 14 | mlockError := unix.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE) 15 | if mlockError != nil { 16 | return fmt.Errorf("mlock error: %w", mlockError) 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /cmd/mlock-non-linux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package cmd 5 | 6 | func EnableMlock() error { 7 | // NOTE: Not required for windows 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Comcast Cable Communications Management, LLC 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | 12 | "github.com/Comcast/Buildenv-Tool/reader" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | const ( 19 | // ErrorCodeMLock Exit Code for MLock Errors 20 | ErrorCodeMLock = 1 21 | // ErrorCodeEnv Exit Code for Missing Environment 22 | ErrorCodeEnv = 2 23 | // ErrorCodeYaml Exit Code for YAML Errors 24 | ErrorCodeYaml = 5 25 | // ErrorCodeVault Exit Code for Vault Errors 26 | ErrorCodeVault = 6 27 | ) 28 | 29 | var cfgFile string 30 | 31 | var Version string 32 | 33 | // rootCmd represents the base command when called without any subcommands 34 | var rootCmd = &cobra.Command{ 35 | Use: "buildenv", 36 | Short: "Set environment variables from a configuation file", 37 | Long: `Set environment variables based on environment and datacenter. 38 | Values can be specified in plain text, or set from a vault server.`, 39 | // Uncomment the following line if your bare application 40 | // has an action associated with it: 41 | Run: func(cmd *cobra.Command, args []string) { 42 | version, _ := cmd.Flags().GetBool("version") 43 | if version { 44 | fmt.Printf("buildenv version %s\n", Version) 45 | os.Exit(0) 46 | } 47 | 48 | ctx := context.Background() 49 | debug, _ := cmd.Flags().GetBool("debug") 50 | 51 | enableMlock, _ := cmd.Flags().GetBool("mlock") 52 | if enableMlock { 53 | err := EnableMlock() 54 | if err != nil { 55 | fmt.Printf("Failure locking memory: %v", err) 56 | os.Exit(ErrorCodeMLock) 57 | } 58 | } 59 | 60 | // Read the Data File 61 | variablesFile, _ := cmd.Flags().GetString("variables_file") 62 | var data reader.Variables 63 | 64 | yamlFile, err := os.ReadFile(variablesFile) 65 | if err != nil { 66 | fmt.Printf("Unable to read file %s: %v", variablesFile, err) 67 | os.Exit(ErrorCodeYaml) 68 | } 69 | err = yaml.Unmarshal(yamlFile, &data) 70 | if err != nil { 71 | fmt.Printf("Unable to parse YAML file %s: %v", variablesFile, err) 72 | } 73 | if debug { 74 | inData, _ := json.MarshalIndent(data, "", " ") 75 | fmt.Printf("Data:\n%s\n\n", inData) 76 | } 77 | 78 | skip_vault, _ := cmd.Flags().GetBool("skip-vault") 79 | 80 | // Setup the Reader 81 | reader, err := reader.NewReader(reader.WithSkipVault(skip_vault)) 82 | if err != nil { 83 | fmt.Printf("Failure creating Reader: %v", err) 84 | os.Exit(ErrorCodeVault) 85 | } 86 | 87 | // Get the Output 88 | env, _ := cmd.Flags().GetString("environment") 89 | run, _ := cmd.Flags().GetString("run") 90 | dc, _ := cmd.Flags().GetString("datacenter") 91 | 92 | out, err := reader.Read(ctx, &data, env, dc) 93 | if err != nil { 94 | fmt.Printf("Failure reading data: %v", err) 95 | os.Exit(ErrorCodeVault) 96 | } 97 | 98 | if debug { 99 | outData, _ := json.MarshalIndent(out, "", " ") 100 | fmt.Printf("Output:\n%s\n\n", outData) 101 | } 102 | 103 | // Output the Exports 104 | comments, _ := cmd.Flags().GetBool("comments") 105 | if cmd.Flags().Lookup("run").Changed { 106 | os.Exit(out.Exec(run)) 107 | } else { 108 | out.Print(comments) 109 | } 110 | }, 111 | } 112 | 113 | // Execute adds all child commands to the root command and sets flags appropriately. 114 | // This is called by main.main(). It only needs to happen once to the rootCmd. 115 | func Execute(version string) { 116 | Version = version 117 | err := rootCmd.Execute() 118 | if err != nil { 119 | os.Exit(1) 120 | } 121 | } 122 | 123 | func init() { 124 | cobra.OnInitialize(initConfig) 125 | 126 | // Here you will define your flags and configuration settings. 127 | // Cobra supports persistent flags, which, if defined here, 128 | // will be global for your application. 129 | 130 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.buildenv.yaml)") 131 | 132 | // Cobra also supports local flags, which will only run 133 | // when this action is called directly. 134 | rootCmd.Flags().StringP("environment", "e", "", "Environment (qa, dev, stage, prod, etc)") 135 | rootCmd.Flags().StringP("run", "r", "", "Shell command to execute with environment") 136 | rootCmd.Flags().StringP("datacenter", "d", "", "Datacenter (ndc_as_a, us-east-1 etc)") 137 | rootCmd.Flags().StringP("variables_file", "f", "variables.yml", "Variables Source YAML file") 138 | 139 | rootCmd.Flags().BoolP("skip-vault", "v", false, "Skip Vault and use only variables file") 140 | rootCmd.Flags().BoolP("mlock", "m", false, "Will enable system mlock if set (prevent write to swap on linux)") 141 | rootCmd.Flags().BoolP("comments", "c", false, "Comments will be included in output") 142 | rootCmd.Flags().Bool("debug", false, "Turn on debugging output") 143 | rootCmd.Flags().Bool("version", false, "Print the version number") 144 | } 145 | 146 | // initConfig reads in config file and ENV variables if set. 147 | func initConfig() { 148 | if cfgFile != "" { 149 | // Use config file from the flag. 150 | viper.SetConfigFile(cfgFile) 151 | } else { 152 | // Find home directory. 153 | home, err := os.UserHomeDir() 154 | cobra.CheckErr(err) 155 | 156 | // Search config in home directory with name ".buildenv" (without extension). 157 | viper.AddConfigPath(home) 158 | viper.SetConfigType("yaml") 159 | viper.SetConfigName(".buildenv") 160 | } 161 | 162 | viper.AutomaticEnv() // read in environment variables that match 163 | 164 | // If a config file is found, read it in. 165 | if err := viper.ReadInConfig(); err == nil { 166 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /cram_tests/inject.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . "$TESTDIR"/setup.sh 4 | 5 | Try injecting quote 6 | 7 | $ echo '{"vars":{"Q": "\"; echo bad \""}}' > test.yml 8 | $ be -f test.yml 9 | export Q="\"; echo bad \"" 10 | 11 | Bad keys 12 | 13 | $ echo '{"vars":{"export hi=there; dosomethingevil && ": "hi"}}' > test2.yml 14 | $ be -f test2.yml 15 | -------------------------------------------------------------------------------- /cram_tests/regress.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . "$TESTDIR"/setup.sh 4 | 5 | Regular run 6 | 7 | $ be -cf "$TESTDIR"/../no_secrets.yml 8 | # Global Variables 9 | export TEST="no secrets" 10 | 11 | -------------------------------------------------------------------------------- /cram_tests/run_option.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ . "$TESTDIR"/setup.sh 4 | 5 | Run buildenv with -r 6 | 7 | $ be -r "echo -n hi" 8 | hi (no-eol) 9 | 10 | Bad command 11 | 12 | $ be -r "./notacommand" 2>/dev/null 13 | [127] 14 | 15 | Vars are there 16 | 17 | $ be -r "echo \${TEST}" 18 | no secrets 19 | -------------------------------------------------------------------------------- /cram_tests/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | alias be="${TESTDIR}/../buildenv -f ${TESTDIR}/../no_secrets.yml" 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | build: 4 | image: golang:1.21-alpine 5 | volumes: 6 | - ./:/go/src/buildenv 7 | vault: 8 | image: hashicorp/vault 9 | cap_add: 10 | - IPC_LOCK 11 | environment: 12 | VAULT_DEV_ROOT_TOKEN_ID: test 13 | ports: 14 | - 8200:8200 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Comcast/Buildenv-Tool 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/hashicorp/vault-client-go v0.4.2 7 | github.com/spf13/cobra v1.8.0 8 | github.com/spf13/viper v1.18.2 9 | golang.org/x/sys v0.15.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/fsnotify/fsnotify v1.7.0 // indirect 15 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 16 | github.com/hashicorp/go-retryablehttp v0.7.1 // indirect 17 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 18 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 19 | github.com/hashicorp/hcl v1.0.0 // indirect 20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 21 | github.com/magiconair/properties v1.8.7 // indirect 22 | github.com/mitchellh/go-homedir v1.1.0 // indirect 23 | github.com/mitchellh/mapstructure v1.5.0 // indirect 24 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 25 | github.com/ryanuber/go-glob v1.0.0 // indirect 26 | github.com/sagikazarmark/locafero v0.4.0 // indirect 27 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 28 | github.com/sourcegraph/conc v0.3.0 // indirect 29 | github.com/spf13/afero v1.11.0 // indirect 30 | github.com/spf13/cast v1.6.0 // indirect 31 | github.com/spf13/pflag v1.0.5 // indirect 32 | github.com/subosito/gotenv v1.6.0 // indirect 33 | go.uber.org/atomic v1.9.0 // indirect 34 | go.uber.org/multierr v1.9.0 // indirect 35 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 36 | golang.org/x/text v0.14.0 // indirect 37 | golang.org/x/time v0.5.0 // indirect 38 | gopkg.in/ini.v1 v1.67.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= 7 | github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= 8 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 9 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 10 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 11 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 13 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 14 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 15 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 16 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 17 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 18 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 19 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 20 | github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= 21 | github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 22 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 23 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 24 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 25 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 26 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 27 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 28 | github.com/hashicorp/vault-client-go v0.4.2 h1:XeUXb5jnDuCUhC8HRpkdGPLh1XtzXmiOnF0mXEbARxI= 29 | github.com/hashicorp/vault-client-go v0.4.2/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY= 30 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 31 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 32 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 33 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 34 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 35 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 36 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 37 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 38 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 39 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 40 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 41 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 42 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 43 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 44 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 45 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 46 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 47 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 50 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 52 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 53 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 54 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 55 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 56 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 57 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 58 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 59 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 60 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 61 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 62 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 63 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 64 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 65 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 66 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 67 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 68 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 69 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 70 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 71 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 74 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 75 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 76 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 77 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 78 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 79 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 80 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 81 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 82 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 83 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 84 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 85 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 86 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 87 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 88 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 89 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 90 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 91 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 92 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 93 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 94 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 97 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 99 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Comcast Cable Communications Management, LLC 3 | */ 4 | package main 5 | 6 | import "github.com/Comcast/Buildenv-Tool/cmd" 7 | 8 | var version string = "development" 9 | 10 | func main() { 11 | cmd.Execute(version) 12 | } 13 | -------------------------------------------------------------------------------- /no_secrets.yml: -------------------------------------------------------------------------------- 1 | --- 2 | vars: 3 | TEST: no secrets -------------------------------------------------------------------------------- /reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "regexp" 11 | "slices" 12 | "strings" 13 | 14 | "github.com/hashicorp/vault-client-go" 15 | ) 16 | 17 | type Reader struct { 18 | client *vault.Client 19 | skipVault bool 20 | canDetectMounts bool 21 | mounts Mounts 22 | } 23 | 24 | type ReaderOptFunc func(*Reader) 25 | 26 | func WithSkipVault(skip bool) ReaderOptFunc { 27 | return func(r *Reader) { 28 | r.skipVault = skip 29 | } 30 | } 31 | 32 | type EnvVars map[string]string 33 | 34 | func (e EnvVars) GetOutput() OutputList { 35 | output := OutputList{} 36 | for k, v := range e { 37 | output = append(output, Output{ 38 | Key: k, 39 | Value: v, 40 | }) 41 | } 42 | return output 43 | } 44 | 45 | type Secrets map[string]string 46 | 47 | var shellvar_regexp = regexp.MustCompile("^[_A-Za-z][A-Za-z0-9_]*$") 48 | 49 | func (s Secrets) GetOutput(ctx context.Context, r *Reader) (OutputList, error) { 50 | // Read it like a kv secrets where all keys are "value" 51 | kvSecrets := KVSecrets{} 52 | for outVar, path := range s { 53 | kvSecret := KVSecretBlock{ 54 | Path: path, 55 | Vars: KVSecret{ 56 | outVar: "value", 57 | }, 58 | } 59 | kvSecrets = append(kvSecrets, kvSecret) 60 | } 61 | return kvSecrets.GetOutput(ctx, r) 62 | } 63 | 64 | type KVSecret map[string]string 65 | 66 | type KVSecretBlock struct { 67 | Path string 68 | Vars KVSecret 69 | } 70 | 71 | type KVSecrets []KVSecretBlock 72 | 73 | func (s KVSecretBlock) GetOutput(ctx context.Context, r *Reader) (OutputList, error) { 74 | output := OutputList{} 75 | 76 | // Initialize the Vault Client if Necessary 77 | if r.client == nil { 78 | err := r.InitVault() 79 | if err != nil { 80 | return nil, err 81 | } 82 | } 83 | 84 | // Get the Mount Point for the Secret 85 | mountPoint, secretPath := r.MountAndPath(s.Path) 86 | if mountPoint == "" { 87 | return nil, fmt.Errorf("no mount point found for path %s", s.Path) 88 | } 89 | 90 | // Assume v2 if we can detect mounts and it's a KV engine, or if it's explicitly v2 91 | if !r.canDetectMounts || (r.mounts[mountPoint].Type == "kv" && r.mounts[mountPoint].Version == "2") { 92 | // Get Secret 93 | resp, err := r.client.Secrets.KvV2Read(ctx, secretPath, vault.WithMountPath(mountPoint)) 94 | if err != nil { 95 | if vault.IsErrorStatus(err, http.StatusNotFound) { 96 | return nil, fmt.Errorf("kv2 secret does not exist: '%s'", s.Path) 97 | } 98 | return nil, fmt.Errorf("error reading kv2 path '%s': %w", s.Path, err) 99 | } 100 | // For testing purposes, we want to order this 101 | envVars := []string{} 102 | for varName := range s.Vars { 103 | envVars = append(envVars, varName) 104 | } 105 | slices.Sort(envVars) 106 | for _, varName := range envVars { 107 | varKey := s.Vars[varName] 108 | if _, hasValue := resp.Data.Data[varKey]; !hasValue { 109 | return nil, fmt.Errorf("key %s not found in path %s", varKey, s.Path) 110 | } 111 | val := fmt.Sprintf("%s", resp.Data.Data[varKey]) 112 | output = append(output, Output{ 113 | Key: varName, 114 | Value: val, 115 | Comment: fmt.Sprintf("Path: %s, Key: %s", s.Path, varKey), 116 | }) 117 | } 118 | } else { 119 | // Treat it as a KVv1 secret 120 | resp, err := r.client.Secrets.KvV1Read(ctx, secretPath, vault.WithMountPath(mountPoint)) 121 | if err != nil { 122 | return nil, fmt.Errorf("error reading kv1 path %s: %w", s.Path, err) 123 | } 124 | for varName, varKey := range s.Vars { 125 | if _, hasValue := resp.Data[varKey]; !hasValue { 126 | return nil, fmt.Errorf("key %s not found in path %s", varKey, s.Path) 127 | } 128 | val := fmt.Sprintf("%s", resp.Data[varKey]) 129 | output = append(output, Output{ 130 | Key: varName, 131 | Value: val, 132 | Comment: fmt.Sprintf("Path: %s, Key: %s", s.Path, varKey), 133 | }) 134 | } 135 | } 136 | 137 | return output, nil 138 | } 139 | 140 | func (s KVSecrets) GetOutput(ctx context.Context, r *Reader) (OutputList, error) { 141 | output := OutputList{} 142 | for _, block := range s { 143 | blockOutput, err := block.GetOutput(ctx, r) 144 | if err != nil { 145 | return nil, err 146 | } 147 | output = append(output, blockOutput...) 148 | } 149 | return output, nil 150 | } 151 | 152 | // KV1Secrets is a list of Key-Value Version 1 Secrets 153 | type KV1Secrets []KV1SecretBlock 154 | 155 | type KV1SecretBlock struct { 156 | Path string 157 | Vars KVSecret 158 | } 159 | 160 | func (s KV1SecretBlock) GetOutput(ctx context.Context, r *Reader) (OutputList, error) { 161 | output := OutputList{} 162 | 163 | // Initialize the Vault Client if Necessary 164 | if r.client == nil { 165 | err := r.InitVault() 166 | if err != nil { 167 | return nil, err 168 | } 169 | } 170 | 171 | // The first thing we need to do is get the mount point for the KV engine 172 | mountPoint, secretPath := r.MountAndPath(s.Path) 173 | if mountPoint == "" { 174 | return nil, fmt.Errorf("no mount point found for path %s", s.Path) 175 | } 176 | 177 | // Treat it as a KVv1 secret 178 | resp, err := r.client.Secrets.KvV1Read(ctx, secretPath, vault.WithMountPath(mountPoint)) 179 | if err != nil { 180 | return nil, fmt.Errorf("error reading kv1 path %s: %w", s.Path, err) 181 | } 182 | for varName, varKey := range s.Vars { 183 | if _, hasValue := resp.Data[varKey]; !hasValue { 184 | return nil, fmt.Errorf("key %s not found in path %s", varKey, s.Path) 185 | } 186 | val := fmt.Sprintf("%s", resp.Data[varKey]) 187 | output = append(output, Output{ 188 | Key: varName, 189 | Value: val, 190 | Comment: fmt.Sprintf("Path: %s, Key: %s", s.Path, varKey), 191 | }) 192 | } 193 | 194 | return output, nil 195 | } 196 | 197 | func (s KV1Secrets) GetOutput(ctx context.Context, r *Reader) (OutputList, error) { 198 | output := OutputList{} 199 | for _, block := range s { 200 | blockOutput, err := block.GetOutput(ctx, r) 201 | if err != nil { 202 | return nil, err 203 | } 204 | output = append(output, blockOutput...) 205 | } 206 | return output, nil 207 | } 208 | 209 | type DC struct { 210 | Vars EnvVars `yaml:"vars,omitempty"` 211 | Secrets Secrets `yaml:"secrets,omitempty"` 212 | KVSecrets KVSecrets `yaml:"kv_secrets,omitempty"` 213 | KV1Secrets KVSecrets `yaml:"kv1_secrets,omitempty"` 214 | } 215 | 216 | type Environment struct { 217 | Vars EnvVars `yaml:"vars,omitempty"` 218 | Secrets Secrets `yaml:"secrets,omitempty"` 219 | KVSecrets KVSecrets `yaml:"kv_secrets,omitempty"` 220 | KV1Secrets KVSecrets `yaml:"kv1_secrets,omitempty"` 221 | Dcs map[string]DC `yaml:"dcs,omitempty"` 222 | } 223 | 224 | type Variables struct { 225 | Vars EnvVars `yaml:"vars,omitempty"` 226 | Secrets Secrets `yaml:"secrets,omitempty"` 227 | KVSecrets KVSecrets `yaml:"kv_secrets,omitempty"` 228 | KV1Secrets KVSecrets `yaml:"kv1_secrets,omitempty"` 229 | Environments map[string]Environment `yaml:"environments,omitempty"` 230 | } 231 | 232 | type Output struct { 233 | Key string 234 | Value string 235 | Comment string 236 | } 237 | type OutputList []Output 238 | 239 | func (o OutputList) Exec(shell_cmd string) int { 240 | shell, shell_isset := os.LookupEnv("SHELL") 241 | 242 | var cmd *exec.Cmd 243 | 244 | if shell_isset { 245 | cmd = exec.Command(shell, "-c", shell_cmd) 246 | } else { 247 | cmd = exec.Command("/usr/bin/env", "bash", "-c", shell_cmd) 248 | } 249 | 250 | for _, out := range o { 251 | if shellvar_regexp.MatchString(out.Key) { 252 | s := fmt.Sprintf("%s=%s", out.Key, out.Value) 253 | cmd.Env = append(cmd.Environ(), s) 254 | } 255 | } 256 | 257 | cmd.Stdin = os.Stdin 258 | cmd.Stdout = os.Stdout 259 | cmd.Stderr = os.Stderr 260 | 261 | if err := cmd.Run(); err != nil { 262 | var exitError *exec.ExitError 263 | if errors.As(err, &exitError) { 264 | return exitError.ExitCode() 265 | } 266 | return -1 267 | } 268 | return 0 269 | } 270 | 271 | func (o OutputList) Print(showComments bool) { 272 | for _, out := range o { 273 | if out.Key == "" { 274 | if showComments && out.Comment != "" { 275 | fmt.Printf("# %s\n", out.Comment) 276 | } 277 | } else { 278 | /* silently discards variable names that are not shell safe */ 279 | if shellvar_regexp.MatchString(out.Key) { 280 | fmt.Printf("export %s=%q", out.Key, out.Value) 281 | if out.Comment != "" && showComments { 282 | fmt.Printf(" # %s", out.Comment) 283 | } 284 | fmt.Println() 285 | } 286 | } 287 | } 288 | } 289 | 290 | type MountInfo struct { 291 | Type string 292 | Version string 293 | } 294 | 295 | type Mounts map[string]MountInfo 296 | 297 | func (r *Reader) InitVault() error { 298 | if r.skipVault { 299 | return nil 300 | } 301 | 302 | vaultClient, err := vault.New(vault.WithEnvironment()) 303 | if err != nil { 304 | return err 305 | } 306 | r.client = vaultClient 307 | r.canDetectMounts = false 308 | 309 | // Get mount info 310 | resp, err := vaultClient.System.MountsListSecretsEngines(context.Background()) 311 | if err == nil { 312 | r.canDetectMounts = true 313 | mounts := Mounts{} 314 | for mount, details := range resp.Data { 315 | detailMap := details.(map[string]interface{}) 316 | thisMount := MountInfo{ 317 | Type: detailMap["type"].(string), 318 | } 319 | if options, hasOptions := detailMap["options"]; hasOptions && options != nil { 320 | optionMap := options.(map[string]interface{}) 321 | if version, hasVersion := optionMap["version"]; hasVersion { 322 | thisMount.Version = version.(string) 323 | } 324 | } 325 | mounts[mount] = thisMount 326 | } 327 | 328 | r.mounts = mounts 329 | } 330 | 331 | return nil 332 | } 333 | 334 | func NewReader(opts ...ReaderOptFunc) (*Reader, error) { 335 | r := &Reader{} 336 | for _, opt := range opts { 337 | opt(r) 338 | } 339 | return r, nil 340 | } 341 | 342 | func (r *Reader) MountAndPath(path string) (string, string) { 343 | if r.canDetectMounts { 344 | for mount := range r.mounts { 345 | if strings.HasPrefix(path, mount) { 346 | return mount, strings.TrimPrefix(path, mount) 347 | } 348 | } 349 | } else { 350 | // Take the first part of the path 351 | parts := strings.SplitN(path, "/", 2) 352 | return parts[0], parts[1] 353 | } 354 | return "", "" 355 | } 356 | 357 | func (r *Reader) Read(ctx context.Context, input *Variables, env string, dc string) (OutputList, error) { 358 | output := OutputList{} 359 | 360 | // Global Variables 361 | output = append(output, Output{ 362 | Comment: "Global Variables", 363 | }) 364 | output = append(output, input.Vars.GetOutput()...) 365 | 366 | if !r.skipVault { 367 | // Global Secrets 368 | kvOut, err := input.KVSecrets.GetOutput(ctx, r) 369 | if err != nil { 370 | return nil, fmt.Errorf("kv secret error: %w", err) 371 | } 372 | output = append(output, kvOut...) 373 | kv1Out, err := input.KV1Secrets.GetOutput(ctx, r) 374 | if err != nil { 375 | return nil, fmt.Errorf("kv1 secret error: %w", err) 376 | } 377 | output = append(output, kv1Out...) 378 | secretOut, err := input.Secrets.GetOutput(ctx, r) 379 | if err != nil { 380 | return nil, fmt.Errorf("secret error: %w", err) 381 | } 382 | output = append(output, secretOut...) 383 | } 384 | 385 | // Environment Variablers 386 | if env != "" { 387 | output = append(output, Output{ 388 | Comment: fmt.Sprintf("Environment: %s", env), 389 | }) 390 | output = append(output, input.Environments[env].Vars.GetOutput()...) 391 | // KV (autodetect or v2) 392 | if !r.skipVault { 393 | kvOut, err := input.Environments[env].KVSecrets.GetOutput(ctx, r) 394 | if err != nil { 395 | return nil, fmt.Errorf("kv secret error: %w", err) 396 | } 397 | output = append(output, kvOut...) 398 | // KV1 399 | kv1Out, err := input.Environments[env].KV1Secrets.GetOutput(ctx, r) 400 | if err != nil { 401 | return nil, fmt.Errorf("kv1 secret error: %w", err) 402 | } 403 | output = append(output, kv1Out...) 404 | // Secrets 405 | secretOut, err := input.Environments[env].Secrets.GetOutput(ctx, r) 406 | if err != nil { 407 | return nil, fmt.Errorf("secret error: %w", err) 408 | } 409 | output = append(output, secretOut...) 410 | } 411 | } 412 | 413 | // DC Variables 414 | if dc != "" { 415 | output = append(output, Output{ 416 | Comment: fmt.Sprintf("Datacenter: %s", dc), 417 | }) 418 | output = append(output, input.Environments[env].Dcs[dc].Vars.GetOutput()...) 419 | 420 | if !r.skipVault { 421 | // KV (autodetect or v2) 422 | kvOut, err := input.Environments[env].Dcs[dc].KVSecrets.GetOutput(ctx, r) 423 | if err != nil { 424 | return nil, fmt.Errorf("kv secret error: %w", err) 425 | } 426 | output = append(output, kvOut...) 427 | // KV1 428 | kv1Out, err := input.Environments[env].Dcs[dc].KV1Secrets.GetOutput(ctx, r) 429 | if err != nil { 430 | return nil, fmt.Errorf("kv1 secret error: %w", err) 431 | } 432 | output = append(output, kv1Out...) 433 | // Secrets 434 | secretOut, err := input.Environments[env].Dcs[dc].Secrets.GetOutput(ctx, r) 435 | if err != nil { 436 | return nil, fmt.Errorf("secret error: %w", err) 437 | } 438 | output = append(output, secretOut...) 439 | } 440 | } 441 | 442 | return output, nil 443 | } 444 | -------------------------------------------------------------------------------- /reader/reader_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/hashicorp/vault-client-go" 15 | ) 16 | 17 | func TestEnvVars_GetOutput(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | e EnvVars 21 | want OutputList 22 | }{ 23 | { 24 | name: "Test Output", 25 | e: EnvVars{ 26 | "a": "b", 27 | }, 28 | want: OutputList{ 29 | { 30 | Key: "a", 31 | Value: "b", 32 | }, 33 | }, 34 | }, 35 | { 36 | name: "Empty Output", 37 | e: EnvVars{}, 38 | want: OutputList{}, 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | if got := tt.e.GetOutput(); !reflect.DeepEqual(got, tt.want) { 44 | t.Errorf("EnvVars.GetOutput() = %v, want %v", got, tt.want) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestReader_Read(t *testing.T) { 51 | type fields struct { 52 | client *vault.Client 53 | } 54 | type args struct { 55 | input *Variables 56 | env string 57 | dc string 58 | } 59 | tests := []struct { 60 | name string 61 | fields fields 62 | args args 63 | want OutputList 64 | wantErr bool 65 | }{ 66 | { 67 | name: "Just Plain Variables", 68 | fields: fields{}, 69 | args: args{ 70 | env: "dev", 71 | dc: "us-least-1", 72 | input: &Variables{ 73 | Vars: EnvVars{ 74 | "FOO": "bar", 75 | }, 76 | Environments: map[string]Environment{ 77 | "dev": { 78 | Vars: EnvVars{ 79 | "ENV": "dev", 80 | }, 81 | Dcs: map[string]DC{ 82 | "us-least-1": { 83 | Vars: EnvVars{ 84 | "DC": "us-least-1", 85 | }, 86 | }, 87 | }, 88 | }, 89 | "stage": { 90 | Vars: EnvVars{ 91 | "env": "stage", 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | want: OutputList{ 98 | { 99 | Comment: "Global Variables", 100 | }, 101 | { 102 | Key: "FOO", 103 | Value: "bar", 104 | }, 105 | { 106 | Comment: "Environment: dev", 107 | }, 108 | { 109 | Key: "ENV", 110 | Value: "dev", 111 | }, 112 | { 113 | Comment: "Datacenter: us-least-1", 114 | }, 115 | { 116 | Key: "DC", 117 | Value: "us-least-1", 118 | }, 119 | }, 120 | wantErr: false, 121 | }, 122 | } 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | ctx := context.Background() 126 | r := &Reader{ 127 | client: tt.fields.client, 128 | } 129 | got, err := r.Read(ctx, tt.args.input, tt.args.env, tt.args.dc) 130 | if (err != nil) != tt.wantErr { 131 | t.Errorf("Reader.Read() error = %v, wantErr %v", err, tt.wantErr) 132 | return 133 | } 134 | if !reflect.DeepEqual(got, tt.want) { 135 | t.Errorf("Reader.Read() = %+v, want %+v", got, tt.want) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestKVSecretBlock_GetOutputNoDetect(t *testing.T) { 142 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 143 | log.Printf("%+v", r) 144 | var resp []byte 145 | status := http.StatusOK 146 | 147 | // KV Data 148 | switch r.URL.Path { 149 | case "/v1/kv2/data/test": 150 | resp = []byte(`{"request_id":"bf3b02c0-096e-84d3-dad7-196aa9f112ed","lease_id":"","renewable":false,"lease_duration":0,"data":{"data":{"one":"1","two":"2","three":"3"},"metadata":{"created_time":"2023-12-20T15:32:32.814115685Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1}},"wrap_info":null,"warnings":null,"auth":null}`) 151 | case "/v1/kv/test": 152 | resp = []byte(`{"request_id":"63c8c31b-f03f-81ac-cfaa-324239789c3f","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"value":"old"},"wrap_info":null,"warnings":null,"auth":null}`) 153 | default: 154 | status = http.StatusNotFound 155 | resp = []byte(`{"errors":[]}`) 156 | } 157 | 158 | w.WriteHeader(status) 159 | w.Write(resp) 160 | })) 161 | defer server.Close() 162 | 163 | client, _ := vault.New(vault.WithAddress(server.URL)) 164 | reader := &Reader{ 165 | client: client, 166 | canDetectMounts: false, 167 | } 168 | 169 | type fields struct { 170 | Path string 171 | Vars KVSecret 172 | } 173 | type args struct { 174 | r *Reader 175 | } 176 | tests := []struct { 177 | name string 178 | fields fields 179 | args args 180 | want OutputList 181 | wantErr bool 182 | }{ 183 | { 184 | name: "No KV Path", 185 | args: args{ 186 | r: reader, 187 | }, 188 | fields: fields{ 189 | Path: "kv2/path", 190 | Vars: KVSecret{ 191 | "NOT": "here", 192 | }, 193 | }, 194 | wantErr: true, 195 | want: nil, 196 | }, 197 | { 198 | name: "No KV2 Key", 199 | args: args{ 200 | r: reader, 201 | }, 202 | fields: fields{ 203 | Path: "kv2/test", 204 | Vars: KVSecret{ 205 | "THREE": "nope", 206 | }, 207 | }, 208 | wantErr: true, 209 | want: nil, 210 | }, 211 | { 212 | name: "With no autodection, KV Read Fails", 213 | args: args{ 214 | r: reader, 215 | }, 216 | fields: fields{ 217 | Path: "kv/test", 218 | Vars: KVSecret{ 219 | "VALUE": "value", 220 | }, 221 | }, 222 | wantErr: true, 223 | }, 224 | { 225 | name: "Test KV2 Read", 226 | args: args{ 227 | r: reader, 228 | }, 229 | fields: fields{ 230 | Path: "kv2/test", 231 | Vars: KVSecret{ 232 | "ONE": "one", 233 | "TWO": "two", 234 | "THREE": "three", 235 | }, 236 | }, 237 | want: OutputList{ 238 | { 239 | Key: "ONE", 240 | Value: "1", 241 | Comment: "Path: kv2/test, Key: one", 242 | }, 243 | { 244 | Key: "THREE", 245 | Value: "3", 246 | Comment: "Path: kv2/test, Key: three", 247 | }, 248 | { 249 | Key: "TWO", 250 | Value: "2", 251 | Comment: "Path: kv2/test, Key: two", 252 | }, 253 | }, 254 | wantErr: false, 255 | }, 256 | } 257 | for _, tt := range tests { 258 | t.Run(tt.name, func(t *testing.T) { 259 | ctx := context.Background() 260 | s := KVSecretBlock{ 261 | Path: tt.fields.Path, 262 | Vars: tt.fields.Vars, 263 | } 264 | got, err := s.GetOutput(ctx, tt.args.r) 265 | if (err != nil) != tt.wantErr { 266 | t.Errorf("KVSecretBlock.GetOutput() error = %v, wantErr %v", err, tt.wantErr) 267 | return 268 | } 269 | if !reflect.DeepEqual(got, tt.want) { 270 | t.Errorf("KVSecretBlock.GetOutput() = %v, want %v", got, tt.want) 271 | } 272 | }) 273 | } 274 | } 275 | 276 | func TestKVSecretBlock_GetOutput(t *testing.T) { 277 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 278 | log.Printf("%+v", r) 279 | var resp []byte 280 | status := http.StatusOK 281 | 282 | // KV Data 283 | switch r.URL.Path { 284 | case "/v1/kv2/data/test": 285 | resp = []byte(`{"request_id":"bf3b02c0-096e-84d3-dad7-196aa9f112ed","lease_id":"","renewable":false,"lease_duration":0,"data":{"data":{"one":"1","two":"2","three":"3"},"metadata":{"created_time":"2023-12-20T15:32:32.814115685Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1}},"wrap_info":null,"warnings":null,"auth":null}`) 286 | case "/v1/kv/test": 287 | resp = []byte(`{"request_id":"63c8c31b-f03f-81ac-cfaa-324239789c3f","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"value":"old"},"wrap_info":null,"warnings":null,"auth":null}`) 288 | default: 289 | status = http.StatusNotFound 290 | resp = []byte(`{"errors":[]}`) 291 | } 292 | 293 | w.WriteHeader(status) 294 | w.Write(resp) 295 | })) 296 | defer server.Close() 297 | 298 | client, _ := vault.New(vault.WithAddress(server.URL)) 299 | reader := &Reader{ 300 | client: client, 301 | canDetectMounts: true, 302 | mounts: Mounts{ 303 | "kv2/": { 304 | Type: "kv", 305 | Version: "2", 306 | }, 307 | "kv/": { 308 | Type: "kv", 309 | }, 310 | "generic/": { 311 | Type: "generic", 312 | }, 313 | }, 314 | } 315 | 316 | type fields struct { 317 | Path string 318 | Vars KVSecret 319 | } 320 | type args struct { 321 | r *Reader 322 | } 323 | tests := []struct { 324 | name string 325 | fields fields 326 | args args 327 | want OutputList 328 | wantErr bool 329 | }{ 330 | { 331 | name: "No Mount", 332 | args: args{ 333 | r: reader, 334 | }, 335 | fields: fields{ 336 | Path: "secret/test", 337 | Vars: KVSecret{ 338 | "should": "fail", 339 | }, 340 | }, 341 | want: nil, 342 | wantErr: true, 343 | }, 344 | { 345 | name: "No KV Path", 346 | args: args{ 347 | r: reader, 348 | }, 349 | fields: fields{ 350 | Path: "kv2/path", 351 | Vars: KVSecret{ 352 | "NOT": "here", 353 | }, 354 | }, 355 | wantErr: true, 356 | want: nil, 357 | }, 358 | { 359 | name: "No KV2 Key", 360 | args: args{ 361 | r: reader, 362 | }, 363 | fields: fields{ 364 | Path: "kv2/test", 365 | Vars: KVSecret{ 366 | "THREE": "nope", 367 | }, 368 | }, 369 | wantErr: true, 370 | want: nil, 371 | }, 372 | { 373 | name: "Test KV Read", 374 | args: args{ 375 | r: reader, 376 | }, 377 | fields: fields{ 378 | Path: "kv/test", 379 | Vars: KVSecret{ 380 | "VALUE": "value", 381 | }, 382 | }, 383 | want: OutputList{ 384 | { 385 | Key: "VALUE", 386 | Value: "old", 387 | Comment: "Path: kv/test, Key: value", 388 | }, 389 | }, 390 | }, 391 | { 392 | name: "Test KV2 Read", 393 | args: args{ 394 | r: reader, 395 | }, 396 | fields: fields{ 397 | Path: "kv2/test", 398 | Vars: KVSecret{ 399 | "ONE": "one", 400 | "TWO": "two", 401 | "THREE": "three", 402 | }, 403 | }, 404 | want: OutputList{ 405 | { 406 | Key: "ONE", 407 | Value: "1", 408 | Comment: "Path: kv2/test, Key: one", 409 | }, 410 | { 411 | Key: "THREE", 412 | Value: "3", 413 | Comment: "Path: kv2/test, Key: three", 414 | }, 415 | { 416 | Key: "TWO", 417 | Value: "2", 418 | Comment: "Path: kv2/test, Key: two", 419 | }, 420 | }, 421 | wantErr: false, 422 | }, 423 | } 424 | for _, tt := range tests { 425 | t.Run(tt.name, func(t *testing.T) { 426 | ctx := context.Background() 427 | s := KVSecretBlock{ 428 | Path: tt.fields.Path, 429 | Vars: tt.fields.Vars, 430 | } 431 | got, err := s.GetOutput(ctx, tt.args.r) 432 | if (err != nil) != tt.wantErr { 433 | t.Errorf("KVSecretBlock.GetOutput() error = %v, wantErr %v", err, tt.wantErr) 434 | return 435 | } 436 | if !reflect.DeepEqual(got, tt.want) { 437 | t.Errorf("KVSecretBlock.GetOutput() = %v, want %v", got, tt.want) 438 | } 439 | }) 440 | } 441 | } 442 | 443 | func TestKV1SecretBlock_GetOutput(t *testing.T) { 444 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 445 | log.Printf("%+v", r) 446 | var resp []byte 447 | status := http.StatusOK 448 | 449 | // KV Data 450 | switch r.URL.Path { 451 | case "/v1/kv/test": 452 | resp = []byte(`{"request_id":"63c8c31b-f03f-81ac-cfaa-324239789c3f","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"value":"old"},"wrap_info":null,"warnings":null,"auth":null}`) 453 | default: 454 | status = http.StatusNotFound 455 | resp = []byte(`{"errors":[]}`) 456 | } 457 | 458 | w.WriteHeader(status) 459 | w.Write(resp) 460 | })) 461 | defer server.Close() 462 | 463 | client, _ := vault.New(vault.WithAddress(server.URL)) 464 | reader := &Reader{ 465 | client: client, 466 | canDetectMounts: true, 467 | mounts: Mounts{ 468 | "kv2/": { 469 | Type: "kv", 470 | Version: "2", 471 | }, 472 | "kv/": { 473 | Type: "kv", 474 | }, 475 | "generic/": { 476 | Type: "generic", 477 | }, 478 | }, 479 | } 480 | 481 | type fields struct { 482 | Path string 483 | Vars KVSecret 484 | } 485 | type args struct { 486 | r *Reader 487 | } 488 | tests := []struct { 489 | name string 490 | fields fields 491 | args args 492 | want OutputList 493 | wantErr bool 494 | }{ 495 | { 496 | name: "No Mount", 497 | args: args{ 498 | r: reader, 499 | }, 500 | fields: fields{ 501 | Path: "secret/test", 502 | Vars: KVSecret{ 503 | "should": "fail", 504 | }, 505 | }, 506 | want: nil, 507 | wantErr: true, 508 | }, 509 | { 510 | name: "No KV Path", 511 | args: args{ 512 | r: reader, 513 | }, 514 | fields: fields{ 515 | Path: "kv/path", 516 | Vars: KVSecret{ 517 | "NOT": "here", 518 | }, 519 | }, 520 | wantErr: true, 521 | want: nil, 522 | }, 523 | { 524 | name: "Test KV Read", 525 | args: args{ 526 | r: reader, 527 | }, 528 | fields: fields{ 529 | Path: "kv/test", 530 | Vars: KVSecret{ 531 | "VALUE": "value", 532 | }, 533 | }, 534 | want: OutputList{ 535 | { 536 | Key: "VALUE", 537 | Value: "old", 538 | Comment: "Path: kv/test, Key: value", 539 | }, 540 | }, 541 | }, 542 | } 543 | for _, tt := range tests { 544 | t.Run(tt.name, func(t *testing.T) { 545 | ctx := context.Background() 546 | s := KV1SecretBlock{ 547 | Path: tt.fields.Path, 548 | Vars: tt.fields.Vars, 549 | } 550 | got, err := s.GetOutput(ctx, tt.args.r) 551 | if (err != nil) != tt.wantErr { 552 | t.Errorf("KVSecretBlock.GetOutput() error = %v, wantErr %v", err, tt.wantErr) 553 | return 554 | } 555 | if !reflect.DeepEqual(got, tt.want) { 556 | t.Errorf("KVSecretBlock.GetOutput() = %v, want %v", got, tt.want) 557 | } 558 | }) 559 | } 560 | } 561 | 562 | func TestSkipVault_Reader(t *testing.T) { 563 | reader, _ := NewReader(WithSkipVault(true)) 564 | 565 | type args struct { 566 | r *Reader 567 | i *Variables 568 | env string 569 | dc string 570 | skip bool 571 | } 572 | 573 | tests := []struct { 574 | name string 575 | args args 576 | want OutputList 577 | wantErr bool 578 | }{ 579 | { 580 | name: "Has Secrets", 581 | args: args{ 582 | skip: true, 583 | env: "dev", 584 | dc: "us-least-1", 585 | r: reader, 586 | i: &Variables{ 587 | Vars: EnvVars{ 588 | "XYZ": "yep", 589 | }, 590 | Secrets: Secrets{ 591 | "Secret1": "it's here", 592 | }, 593 | KVSecrets: KVSecrets{{ 594 | Path: "path/test", 595 | Vars: KVSecret{"KVSecret1": "kvsecret1"}, 596 | }}, 597 | KV1Secrets: KVSecrets{{ 598 | Path: "path2/test", 599 | Vars: KVSecret{ 600 | "KV1Secret1": "another one", 601 | }, 602 | }}, 603 | }, 604 | }, 605 | want: OutputList{ 606 | { 607 | Comment: "Global Variables", 608 | }, 609 | {Key: "XYZ", Value: "yep", Comment: ""}, 610 | { 611 | Comment: "Environment: dev", 612 | }, 613 | { 614 | Comment: "Datacenter: us-least-1", 615 | }, 616 | }, 617 | wantErr: false, 618 | }, 619 | } 620 | for _, tt := range tests { 621 | t.Run(tt.name, func(t *testing.T) { 622 | ctx := context.Background() 623 | got, err := tt.args.r.Read(ctx, tt.args.i, tt.args.env, tt.args.dc) 624 | if (err != nil) != tt.wantErr { 625 | t.Errorf("Reader.Read() error = %v, wantErr %v", err, tt.wantErr) 626 | return 627 | } 628 | if !reflect.DeepEqual(got, tt.want) { 629 | t.Errorf("Reader.Read() = %v, want %v", got, tt.want) 630 | } 631 | }) 632 | } 633 | } 634 | 635 | func TestOutputList_Exec(t *testing.T) { 636 | key := "BuildEnvTestKey" 637 | val := "BuildEnvTestVal" 638 | outputList := OutputList{ 639 | Output{key, val, "acomment"}, 640 | } 641 | 642 | type fields struct { 643 | Key string 644 | Val string 645 | Out OutputList 646 | } 647 | 648 | type args struct { 649 | cmd string 650 | } 651 | 652 | tests := []struct { 653 | name string 654 | fields fields 655 | args args 656 | want string 657 | wantErr int 658 | }{ 659 | { 660 | name: "Good Simple Cmd", 661 | args: args{ 662 | cmd: "echo -n hi", 663 | }, 664 | fields: fields{ 665 | Key: key, 666 | Val: val, 667 | Out: outputList, 668 | }, 669 | want: "hi", 670 | wantErr: 0, 671 | }, 672 | { 673 | name: "Bad Cmd", 674 | args: args{ 675 | cmd: "./nosuchcommandprobably", 676 | }, 677 | fields: fields{ 678 | Key: key, 679 | Val: val, 680 | Out: outputList, 681 | }, 682 | want: "", 683 | wantErr: 127, 684 | }, 685 | { 686 | name: "Has Env Var val", 687 | args: args{ 688 | cmd: "echo -n ${" + key + "}", 689 | }, 690 | fields: fields{ 691 | Key: key, 692 | Val: val, 693 | Out: outputList, 694 | }, 695 | want: val, 696 | wantErr: 0, 697 | }, 698 | } 699 | 700 | for _, tt := range tests { 701 | t.Run(tt.name, func(t *testing.T) { 702 | oldstdout := os.Stdout 703 | r, w, _ := os.Pipe() 704 | os.Stdout = w 705 | got := tt.fields.Out.Exec(tt.args.cmd) 706 | 707 | outC := make(chan string) 708 | 709 | go func() { 710 | var buf bytes.Buffer 711 | io.Copy(&buf, r) 712 | outC <- buf.String() 713 | }() 714 | 715 | w.Close() 716 | 717 | out := <-outC 718 | 719 | os.Stdout = oldstdout 720 | 721 | if out != tt.want { 722 | t.Errorf("OutputList.Exec() output = %v, want %v", out, tt.want) 723 | } 724 | if got != tt.wantErr { 725 | t.Errorf("OutputList.Exec() err = %v, wantErr %v", got, tt.wantErr) 726 | } 727 | }) 728 | } 729 | } 730 | -------------------------------------------------------------------------------- /variables.yml: -------------------------------------------------------------------------------- 1 | --- 2 | vars: 3 | GLOBAL: "global" 4 | 5 | secrets: 6 | GENERIC_SECRET: "gen/test" 7 | KV_SECRET: "old/test" 8 | KV2_SECRET: "secret/oldstyle" 9 | 10 | kv_secrets: 11 | - path: "secret/test" 12 | vars: 13 | KV2_ONE: "one" 14 | KV2_TWO: "two" 15 | - path: "old/test" 16 | vars: 17 | KV1: "value" 18 | - path: "gen/test" 19 | vars: 20 | KV_GENERIC: "value" 21 | 22 | kv1_secrets: 23 | - path: "old/test" 24 | vars: 25 | KV1SPECIFIC: "value" 26 | 27 | environments: 28 | stage: 29 | vars: 30 | ENVIRONMENT: "stage" 31 | 32 | secrets: 33 | ANOTHER_SECRET: "secret/oldstyle" 34 | 35 | dcs: 36 | ndc_one: 37 | vars: 38 | DC: "one" 39 | kv_secrets: 40 | - path: "old/test" 41 | vars: 42 | KV2_THREE: "three" 43 | 44 | 45 | --------------------------------------------------------------------------------