├── .dockerignore ├── .gitignore ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Readme.md ├── generate-mocks.sh ├── main.go ├── mocks └── api │ └── mock_vault_client.go └── pkg ├── api └── vault_client.go └── template ├── template.go └── template_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | vault-template 3 | Dockerfile 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | vault-template 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11-alpine as build 2 | 3 | # install go dep (and git which is needed by dep) 4 | RUN apk update && apk add --no-cache git ca-certificates 5 | RUN wget https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -O /usr/local/bin/dep 6 | RUN chmod +x /usr/local/bin/dep 7 | 8 | # install dependencies 9 | WORKDIR $GOPATH/src/github.com/actano/vault-template 10 | COPY Gopkg.toml Gopkg.lock ./ 11 | RUN dep ensure --vendor-only 12 | 13 | # build binary 14 | COPY . ./ 15 | RUN CGO_ENABLED=0 GOOS=linux go test ./... 16 | RUN CGO_ENABLED=0 GOOS=linux go build -o /vault-template 17 | 18 | FROM scratch 19 | 20 | COPY --from=build /etc/ssl/certs /etc/ssl/certs 21 | COPY --from=build /vault-template /vault-template 22 | 23 | ENTRYPOINT ["/vault-template"] 24 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:7e67dac6b9a4d199acd4fb7af53e9741975130cd8a7b6e5c25e542933413565d" 6 | name = "github.com/Luzifer/rconfig" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "5b80190bff90ccb9899db31e45baac7b1bede03b" 10 | version = "v2.2.0" 11 | 12 | [[projects]] 13 | digest = "1:55388fd080150b9a072912f97b1f5891eb0b50df43401f8b75fb4273d3fec9fc" 14 | name = "github.com/Masterminds/semver" 15 | packages = ["."] 16 | pruneopts = "UT" 17 | revision = "c7af12943936e8c39859482e61f0574c2fd7fc75" 18 | version = "v1.4.2" 19 | 20 | [[projects]] 21 | digest = "1:e6cdc43eba27f9d29f0d6497414597e45ea812c2e51ffd42a1f8f1bb0c1a9d98" 22 | name = "github.com/Masterminds/sprig" 23 | packages = ["."] 24 | pruneopts = "UT" 25 | revision = "544a9b1d90f323f6509491b389714fbbd126bee3" 26 | version = "v2.17.1" 27 | 28 | [[projects]] 29 | digest = "1:3b10c6fd33854dc41de2cf78b7bae105da94c2789b6fa5b9ac9e593ea43484ac" 30 | name = "github.com/aokoli/goutils" 31 | packages = ["."] 32 | pruneopts = "UT" 33 | revision = "41ac8693c5c10a92ea1ff5ac3a7f95646f6123b0" 34 | version = "v1.1.0" 35 | 36 | [[projects]] 37 | digest = "1:b60efdeb75d3c0ceed88783ac2495256aba3491a537d0f31401202579fd62a94" 38 | name = "github.com/golang/mock" 39 | packages = ["gomock"] 40 | pruneopts = "UT" 41 | revision = "51421b967af1f557f93a59e0057aaf15ca02e29c" 42 | version = "v1.2.0" 43 | 44 | [[projects]] 45 | branch = "master" 46 | digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009" 47 | name = "github.com/golang/snappy" 48 | packages = ["."] 49 | pruneopts = "UT" 50 | revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" 51 | 52 | [[projects]] 53 | digest = "1:2e3c336fc7fde5c984d2841455a658a6d626450b1754a854b3b32e7a8f49a07a" 54 | name = "github.com/google/go-cmp" 55 | packages = [ 56 | "cmp", 57 | "cmp/internal/diff", 58 | "cmp/internal/function", 59 | "cmp/internal/value", 60 | ] 61 | pruneopts = "UT" 62 | revision = "3af367b6b30c263d47e8895973edcca9a49cf029" 63 | version = "v0.2.0" 64 | 65 | [[projects]] 66 | digest = "1:236d7e1bdb50d8f68559af37dbcf9d142d56b431c9b2176d41e2a009b664cda8" 67 | name = "github.com/google/uuid" 68 | packages = ["."] 69 | pruneopts = "UT" 70 | revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8" 71 | version = "v1.1.0" 72 | 73 | [[projects]] 74 | digest = "1:0ade334594e69404d80d9d323445d2297ff8161637f9b2d347cc6973d2d6f05b" 75 | name = "github.com/hashicorp/errwrap" 76 | packages = ["."] 77 | pruneopts = "UT" 78 | revision = "8a6fb523712970c966eefc6b39ed2c5e74880354" 79 | version = "v1.0.0" 80 | 81 | [[projects]] 82 | digest = "1:f47d6109c2034cb16bd62b220e18afd5aa9d5a1630fe5d937ad96a4fb7cbb277" 83 | name = "github.com/hashicorp/go-cleanhttp" 84 | packages = ["."] 85 | pruneopts = "UT" 86 | revision = "e8ab9daed8d1ddd2d3c4efba338fe2eeae2e4f18" 87 | version = "v0.5.0" 88 | 89 | [[projects]] 90 | digest = "1:f668349b83f7d779567c880550534addeca7ebadfdcf44b0b9c39be61864b4b7" 91 | name = "github.com/hashicorp/go-multierror" 92 | packages = ["."] 93 | pruneopts = "UT" 94 | revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1" 95 | version = "v1.0.0" 96 | 97 | [[projects]] 98 | digest = "1:4112546e6964796e1c92a9ffdea8fd7ae81ffbf81eda4f946f50937e178f53da" 99 | name = "github.com/hashicorp/go-retryablehttp" 100 | packages = ["."] 101 | pruneopts = "UT" 102 | revision = "4502c0ecdaf0b50d857611af23831260f99be6bf" 103 | version = "v0.5.0" 104 | 105 | [[projects]] 106 | branch = "master" 107 | digest = "1:45aad874d3c7d5e8610427c81870fb54970b981692930ec2a319ce4cb89d7a00" 108 | name = "github.com/hashicorp/go-rootcerts" 109 | packages = ["."] 110 | pruneopts = "UT" 111 | revision = "6bb64b370b90e7ef1fa532be9e591a81c3493e00" 112 | 113 | [[projects]] 114 | branch = "master" 115 | digest = "1:14f2005c31ddf99c4a0f36fc440f8d1ac43224194c7c4a904b3c8f4ba5654d0b" 116 | name = "github.com/hashicorp/go-sockaddr" 117 | packages = ["."] 118 | pruneopts = "UT" 119 | revision = "6d291a969b86c4b633730bfc6b8b9d64c3aafed9" 120 | 121 | [[projects]] 122 | digest = "1:ea40c24cdbacd054a6ae9de03e62c5f252479b96c716375aace5c120d68647c8" 123 | name = "github.com/hashicorp/hcl" 124 | packages = [ 125 | ".", 126 | "hcl/ast", 127 | "hcl/parser", 128 | "hcl/scanner", 129 | "hcl/strconv", 130 | "hcl/token", 131 | "json/parser", 132 | "json/scanner", 133 | "json/token", 134 | ] 135 | pruneopts = "UT" 136 | revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" 137 | version = "v1.0.0" 138 | 139 | [[projects]] 140 | digest = "1:855a0cb08277efb9b18fee68f6f94259775df36350276257df8c96e9d51dbaaf" 141 | name = "github.com/hashicorp/vault" 142 | packages = [ 143 | "api", 144 | "helper/compressutil", 145 | "helper/consts", 146 | "helper/hclutil", 147 | "helper/jsonutil", 148 | "helper/parseutil", 149 | "helper/strutil", 150 | ] 151 | pruneopts = "UT" 152 | revision = "c19cef14891751a23eaa9b41fd456d1f99e7e856" 153 | version = "v1.0.0" 154 | 155 | [[projects]] 156 | digest = "1:f9a5e090336881be43cfc1cf468330c1bdd60abdc9dd194e0b1ab69f4b94dd7c" 157 | name = "github.com/huandu/xstrings" 158 | packages = ["."] 159 | pruneopts = "UT" 160 | revision = "f02667b379e2fb5916c3cda2cf31e0eb885d79f8" 161 | version = "v1.2.0" 162 | 163 | [[projects]] 164 | digest = "1:8eb1de8112c9924d59bf1d3e5c26f5eaa2bfc2a5fcbb92dc1c2e4546d695f277" 165 | name = "github.com/imdario/mergo" 166 | packages = ["."] 167 | pruneopts = "UT" 168 | revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4" 169 | version = "v0.3.6" 170 | 171 | [[projects]] 172 | digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af" 173 | name = "github.com/mitchellh/go-homedir" 174 | packages = ["."] 175 | pruneopts = "UT" 176 | revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4" 177 | version = "v1.0.0" 178 | 179 | [[projects]] 180 | digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" 181 | name = "github.com/mitchellh/mapstructure" 182 | packages = ["."] 183 | pruneopts = "UT" 184 | revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" 185 | version = "v1.1.2" 186 | 187 | [[projects]] 188 | digest = "1:e39a5ee8fcbec487f8fc68863ef95f2b025e0739b0e4aa55558a2b4cf8f0ecf0" 189 | name = "github.com/pierrec/lz4" 190 | packages = [ 191 | ".", 192 | "internal/xxh32", 193 | ] 194 | pruneopts = "UT" 195 | revision = "635575b42742856941dbc767b44905bb9ba083f6" 196 | version = "v2.0.7" 197 | 198 | [[projects]] 199 | digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" 200 | name = "github.com/pkg/errors" 201 | packages = ["."] 202 | pruneopts = "UT" 203 | revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" 204 | version = "v0.8.1" 205 | 206 | [[projects]] 207 | digest = "1:0e792eea6c96ec55ff302ef33886acbaa5006e900fefe82689e88d96439dcd84" 208 | name = "github.com/ryanuber/go-glob" 209 | packages = ["."] 210 | pruneopts = "UT" 211 | revision = "572520ed46dbddaed19ea3d9541bdd0494163693" 212 | version = "v0.1" 213 | 214 | [[projects]] 215 | digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" 216 | name = "github.com/spf13/pflag" 217 | packages = ["."] 218 | pruneopts = "UT" 219 | revision = "298182f68c66c05229eb03ac171abe6e309ee79a" 220 | version = "v1.0.3" 221 | 222 | [[projects]] 223 | branch = "master" 224 | digest = "1:dd658e9008df9e0bea1ddb2caa3df8250e5e42bf54dcb776ed5bef0496395549" 225 | name = "golang.org/x/crypto" 226 | packages = [ 227 | "pbkdf2", 228 | "scrypt", 229 | ] 230 | pruneopts = "UT" 231 | revision = "ff983b9c42bc9fbf91556e191cc8efb585c16908" 232 | 233 | [[projects]] 234 | branch = "master" 235 | digest = "1:f8292d035e3f59cbcf39fecb9ca1dd24ec20ae4f8a06750609ee4b71d60d0045" 236 | name = "golang.org/x/net" 237 | packages = [ 238 | "http/httpguts", 239 | "http2", 240 | "http2/hpack", 241 | "idna", 242 | ] 243 | pruneopts = "UT" 244 | revision = "351d144fa1fc0bd934e2408202be0c29f25e35a0" 245 | 246 | [[projects]] 247 | digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" 248 | name = "golang.org/x/text" 249 | packages = [ 250 | "collate", 251 | "collate/build", 252 | "internal/colltab", 253 | "internal/gen", 254 | "internal/tag", 255 | "internal/triegen", 256 | "internal/ucd", 257 | "language", 258 | "secure/bidirule", 259 | "transform", 260 | "unicode/bidi", 261 | "unicode/cldr", 262 | "unicode/norm", 263 | "unicode/rangetable", 264 | ] 265 | pruneopts = "UT" 266 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 267 | version = "v0.3.0" 268 | 269 | [[projects]] 270 | branch = "master" 271 | digest = "1:9fdc2b55e8e0fafe4b41884091e51e77344f7dc511c5acedcfd98200003bff90" 272 | name = "golang.org/x/time" 273 | packages = ["rate"] 274 | pruneopts = "UT" 275 | revision = "85acf8d2951cb2a3bde7632f9ff273ef0379bcbd" 276 | 277 | [[projects]] 278 | branch = "v2" 279 | digest = "1:b6539350da50de0d3c9b83ae587c06b89be9cb5750443bdd887f1c4077f57776" 280 | name = "gopkg.in/validator.v2" 281 | packages = ["."] 282 | pruneopts = "UT" 283 | revision = "135c24b11c19e52befcae2ec3fca5d9b78c4e98e" 284 | 285 | [[projects]] 286 | digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96" 287 | name = "gopkg.in/yaml.v2" 288 | packages = ["."] 289 | pruneopts = "UT" 290 | revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" 291 | version = "v2.2.2" 292 | 293 | [[projects]] 294 | digest = "1:a5cc901b8e54c4b73a62f97dc7c8f6c46e347c148a7a38e09b2e942c2587ba03" 295 | name = "gotest.tools" 296 | packages = [ 297 | "assert", 298 | "assert/cmp", 299 | "internal/difflib", 300 | "internal/format", 301 | "internal/source", 302 | ] 303 | pruneopts = "UT" 304 | revision = "1083505acf35a0bd8a696b26837e1fb3187a7a83" 305 | version = "v2.3.0" 306 | 307 | [solve-meta] 308 | analyzer-name = "dep" 309 | analyzer-version = 1 310 | input-imports = [ 311 | "github.com/Luzifer/rconfig", 312 | "github.com/Masterminds/sprig", 313 | "github.com/golang/mock/gomock", 314 | "github.com/hashicorp/vault/api", 315 | "gotest.tools/assert", 316 | ] 317 | solver-name = "gps-cdcl" 318 | solver-version = 1 319 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [prune] 2 | go-tests = true 3 | unused-packages = true 4 | 5 | [[constraint]] 6 | name = "github.com/Luzifer/rconfig" 7 | version = "2.2.0" 8 | 9 | [[constraint]] 10 | name = "github.com/hashicorp/vault" 11 | version = "1.0.0" 12 | 13 | [[constraint]] 14 | name = "github.com/Masterminds/sprig" 15 | version = "2.17.1" 16 | 17 | [[constraint]] 18 | name = "github.com/golang/mock" 19 | version = "1.2.0" 20 | 21 | [[constraint]] 22 | name = "gotest.tools" 23 | version = "2.3.0" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Actano GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # `vault-template` 2 | Responsible: #DevOps 3 | Render templated config files with secrets from [HashiCorp Vault](https://www.vaultproject.io/). Inspired by [vaultenv](https://github.com/channable/vaultenv). 4 | 5 | * Define a template for your config file which contains secrets at development time. 6 | * Use `vault-template` to render your config file template by fetching secrets from Vault at runtime. 7 | 8 | ## Usage 9 | 10 | ```text 11 | Usage of ./vault-template: 12 | -o, --output string The output file. 13 | Also configurable via OUTPUT_FILE. 14 | -t, --template string The template file to render. 15 | Also configurable via TEMPLATE_FILE. 16 | -v, --vault string Vault API endpoint. 17 | Also configurable via VAULT_ADDR. 18 | (default "http://127.0.0.1:8200") 19 | -f, --vault-token-file string The file which contains the vault token. 20 | Also configurable via VAULT_TOKEN_FILE. 21 | ``` 22 | 23 | A [docker image is availabe on Dockerhub.](https://hub.docker.com/r/rplan/vault-template) 24 | 25 | ## Template 26 | 27 | First of all, suppose that the secret was created with `vault write secret/mySecret name=john password=secret`. 28 | 29 | The templates will be rendered using the [Go template](https://golang.org/pkg/text/template/) mechanism. 30 | 31 | Currently vault-template can render two functions: 32 | - `vault` 33 | - `vaultMap` 34 | 35 | Also it is possible to use environment variables like `{{ .STAGE }}`. 36 | 37 | The `vault` function takes two string parameters which specify the path to the secret and the field inside to return. 38 | 39 | ```gotemplate 40 | mySecretName = {{ vault "secret/mySecret" "name" }} 41 | mySecretPassword = {{ vault "secret/mySecret" "password" }} 42 | ``` 43 | 44 | ```text 45 | mySecretName = john 46 | mySecretPassword = secret 47 | ``` 48 | 49 | The `vaultMap` function takes one string parameter which specify the path to the secret to return. 50 | 51 | ```gotemplate 52 | {{ range $name, $secret := vaultMap "secret/mySecret"}} 53 | {{ $name }}: {{ $secret }} 54 | {{- end }} 55 | ``` 56 | 57 | ```text 58 | name: john 59 | password: secret 60 | ``` 61 | 62 | More real example: 63 | 64 | ```gotemplate 65 | --- 66 | # Common vars 67 | {{- $customer := .CUSTOMER }} 68 | {{- $stage := .STAGE }} 69 | {{- $project := .PROJECT }} 70 | {{- $postgres := print "kv/data/" $customer "/" $stage "/" $project "/postgres" }} 71 | {{- $postgresMap := vaultMap $postgres }} 72 | 73 | postgresql: 74 | postgresqlUsername: {{ $postgresMap.data.user }} 75 | postgresqlPassword: {{ $postgresMap.data.password }} 76 | postgresqlDatabase: {{ $postgresMap.data.db }} 77 | 78 | app: 79 | postgres: 80 | {{ range $name, $secret := $postgresMap }} 81 | {{ $name }}: {{ $secret }} 82 | {{- end }} 83 | ``` 84 | 85 | And command that use this template in kubernetes: 86 | ``` 87 | CUSTOMER=internal STAGE=test PROJECT=myprj vault-template -o values.yaml -t values.tmpl -v "http://vault.default.svc.cluster.local:8200" -f token 88 | ``` 89 | -------------------------------------------------------------------------------- /generate-mocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mockgen -destination=mocks/api/mock_vault_client.go -package=api github.com/actano/vault-template/api VaultClient 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Luzifer/rconfig" 5 | "github.com/actano/vault-template/pkg/template" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | ) 10 | 11 | var ( 12 | cfg = struct { 13 | VaultEndpoint string `flag:"vault,v" env:"VAULT_ADDR" default:"https://127.0.0.1:8200" description:"Vault API endpoint. Also configurable via VAULT_ADDR."` 14 | VaultTokenFile string `flag:"vault-token-file,f" env:"VAULT_TOKEN_FILE" description:"The file which contains the vault token. Also configurable via VAULT_TOKEN_FILE."` 15 | TemplateFile string `flag:"template,t" env:"TEMPLATE_FILE" description:"The template file to render. Also configurable via TEMPLATE_FILE."` 16 | OutputFile string `flag:"output,o" env:"OUTPUT_FILE" description:"The output file. Also configurable via OUTPUT_FILE."` 17 | }{} 18 | ) 19 | 20 | func usage(msg string) { 21 | println(msg) 22 | rconfig.Usage() 23 | os.Exit(1) 24 | } 25 | 26 | func config() { 27 | err := rconfig.Parse(&cfg) 28 | 29 | if err != nil { 30 | log.Fatalf("Error while parsing the command line arguments: %s", err) 31 | } 32 | 33 | if cfg.VaultTokenFile == "" { 34 | usage("No vault token file given") 35 | } 36 | 37 | if cfg.TemplateFile == "" { 38 | usage("No template file given") 39 | } 40 | 41 | if cfg.OutputFile == "" { 42 | usage("No output file given") 43 | } 44 | } 45 | 46 | func main() { 47 | config() 48 | 49 | vaultToken, err := ioutil.ReadFile(cfg.VaultTokenFile) 50 | 51 | if err != nil { 52 | log.Fatalf("Unable to read vault token file: %s", err) 53 | } 54 | 55 | renderer, err := template.NewVaultTemplateRenderer(string(vaultToken), cfg.VaultEndpoint) 56 | 57 | if err != nil { 58 | log.Fatalf("Unable to create renderer: %s", err) 59 | } 60 | 61 | templateContent, err := ioutil.ReadFile(cfg.TemplateFile) 62 | 63 | if err != nil { 64 | log.Fatalf("Unable to read template file: %s", err) 65 | } 66 | 67 | renderedContent, err := renderer.RenderTemplate(string(templateContent)) 68 | 69 | if err != nil { 70 | log.Fatalf("Unable to render template: %s", err) 71 | } 72 | 73 | outputFile, err := os.Create(cfg.OutputFile) 74 | 75 | if err != nil { 76 | log.Fatalf("Unable to write output file: %s", err) 77 | } 78 | 79 | defer func() { 80 | err := outputFile.Close() 81 | if err != nil { 82 | log.Fatalf("Error while closing the output file: %s", err) 83 | } 84 | }() 85 | 86 | _, err = outputFile.Write([]byte(renderedContent)) 87 | 88 | if err != nil { 89 | log.Fatalf("Error while writing the output file: %s", err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /mocks/api/mock_vault_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/actano/vault-template/api (interfaces: VaultClient) 3 | 4 | // Package api is a generated GoMock package. 5 | package api 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockVaultClient is a mock of VaultClient interface 13 | type MockVaultClient struct { 14 | ctrl *gomock.Controller 15 | recorder *MockVaultClientMockRecorder 16 | } 17 | 18 | // MockVaultClientMockRecorder is the mock recorder for MockVaultClient 19 | type MockVaultClientMockRecorder struct { 20 | mock *MockVaultClient 21 | } 22 | 23 | // NewMockVaultClient creates a new mock instance 24 | func NewMockVaultClient(ctrl *gomock.Controller) *MockVaultClient { 25 | mock := &MockVaultClient{ctrl: ctrl} 26 | mock.recorder = &MockVaultClientMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockVaultClient) EXPECT() *MockVaultClientMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // QuerySecretMap mocks base method 36 | func (m *MockVaultClient) QuerySecretMap(arg0 string) (map[string]interface{}, error) { 37 | m.ctrl.T.Helper() 38 | ret := m.ctrl.Call(m, "QuerySecretMap", arg0) 39 | ret0, _ := ret[0].(map[string]interface{}) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // QuerySecret mocks base method 45 | func (m *MockVaultClient) QuerySecret(arg0, arg1 string) (string, error) { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "QuerySecret", arg0, arg1) 48 | ret0, _ := ret[0].(string) 49 | ret1, _ := ret[1].(error) 50 | return ret0, ret1 51 | } 52 | 53 | // QuerySecret indicates an expected call of QuerySecret 54 | func (mr *MockVaultClientMockRecorder) QuerySecret(arg0, arg1 interface{}) *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QuerySecret", reflect.TypeOf((*MockVaultClient)(nil).QuerySecret), arg0, arg1) 57 | } 58 | 59 | // QuerySecretMap indicates an expected call of QuerySecretMap 60 | func (mr *MockVaultClientMockRecorder) QuerySecretMap(arg0 interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QuerySecretMap", reflect.TypeOf((*MockVaultClient)(nil).QuerySecretMap), arg0) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/api/vault_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/vault/api" 6 | "strings" 7 | ) 8 | 9 | type VaultClient interface { 10 | QuerySecret(path string, field string) (string, error) 11 | QuerySecretMap(path string) (map[string]interface{}, error) 12 | } 13 | 14 | type vaultClient struct { 15 | apiClient *api.Client 16 | } 17 | 18 | func NewVaultClient(vaultEndpoint string, vaultToken string) (VaultClient, error) { 19 | apiClient, err := api.NewClient(&api.Config{ 20 | Address: vaultEndpoint, 21 | }) 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | apiClient.SetToken(strings.TrimSpace(vaultToken)) 28 | 29 | vaultClient := &vaultClient{ 30 | apiClient: apiClient, 31 | } 32 | 33 | return vaultClient, nil 34 | } 35 | 36 | func (c *vaultClient) QuerySecretMap(path string) (map[string]interface{}, error) { 37 | secret, err := c.apiClient.Logical().Read(path) 38 | 39 | if err != nil { 40 | return nil, err 41 | } 42 | if secret == nil { 43 | return nil, fmt.Errorf("path '%s' is not found", path) 44 | } 45 | 46 | return secret.Data, nil 47 | } 48 | 49 | func (c *vaultClient) QuerySecret(path string, field string) (string, error) { 50 | secret, err := c.apiClient.Logical().Read(path) 51 | 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | secretValue, ok := secret.Data[field] 57 | 58 | if !ok { 59 | return "", fmt.Errorf("secret at path '%s' has no field '%s'", path, field) 60 | } 61 | 62 | return secretValue.(string), nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/template/template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "github.com/Masterminds/sprig" 6 | "github.com/actano/vault-template/pkg/api" 7 | "os" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | type VaultTemplateRenderer struct { 13 | vaultClient api.VaultClient 14 | } 15 | 16 | func NewVaultTemplateRenderer(vaultToken, vaultEndpoint string) (*VaultTemplateRenderer, error) { 17 | vaultClient, err := api.NewVaultClient(vaultEndpoint, string(vaultToken)) 18 | 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &VaultTemplateRenderer{ 24 | vaultClient: vaultClient, 25 | }, nil 26 | } 27 | 28 | func (v *VaultTemplateRenderer) RenderTemplate(templateContent string) (string, error) { 29 | funcMap := template.FuncMap{ 30 | "vault": v.vaultClient.QuerySecret, 31 | "vaultMap": v.vaultClient.QuerySecretMap, 32 | } 33 | 34 | tmpl, err := template. 35 | New("template"). 36 | Funcs(sprig.TxtFuncMap()). 37 | Funcs(funcMap). 38 | Parse(templateContent) 39 | 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | var outputBuffer bytes.Buffer 45 | 46 | envMap := envToMap() 47 | if err := tmpl.Execute(&outputBuffer, envMap); err != nil { 48 | return "", err 49 | } 50 | 51 | return outputBuffer.String(), nil 52 | } 53 | 54 | func envToMap() map[string]string { 55 | envMap := map[string]string{} 56 | 57 | for _, v := range os.Environ() { 58 | splitV := strings.Split(v, "=") 59 | envMap[splitV[0]] = splitV[1] 60 | } 61 | 62 | return envMap 63 | } 64 | -------------------------------------------------------------------------------- /pkg/template/template_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "github.com/actano/vault-template/mocks/api" 5 | "github.com/golang/mock/gomock" 6 | "github.com/pkg/errors" 7 | "gotest.tools/assert" 8 | "testing" 9 | ) 10 | 11 | func TestRenderTemplate(t *testing.T) { 12 | mockCtrl := gomock.NewController(t) 13 | defer mockCtrl.Finish() 14 | 15 | mockVaultClient := api.NewMockVaultClient(mockCtrl) 16 | 17 | mockVaultClient. 18 | EXPECT(). 19 | QuerySecret("secret/my/test/secret", "field1"). 20 | Return("secret1", nil). 21 | Times(1) 22 | 23 | template := "The secret is '{{ vault \"secret/my/test/secret\" \"field1\" }}'." 24 | 25 | renderer := VaultTemplateRenderer{ 26 | vaultClient: mockVaultClient, 27 | } 28 | 29 | result, err := renderer.RenderTemplate(template) 30 | 31 | assert.NilError(t, err) 32 | assert.Equal(t, result, "The secret is 'secret1'.") 33 | } 34 | 35 | func TestRenderMapTemplate(t *testing.T) { 36 | mockCtrl := gomock.NewController(t) 37 | defer mockCtrl.Finish() 38 | 39 | mockVaultClient := api.NewMockVaultClient(mockCtrl) 40 | 41 | mockVaultClient. 42 | EXPECT(). 43 | QuerySecretMap("secret/my/test/secret"). 44 | Return(map[string]interface{}{"field1": "secret1"}, nil). 45 | Times(1) 46 | 47 | template := "{{ $m := vaultMap \"secret/my/test/secret\" }}The secret is '{{ $m.field1 }}'." 48 | 49 | renderer := VaultTemplateRenderer{ 50 | vaultClient: mockVaultClient, 51 | } 52 | 53 | result, err := renderer.RenderTemplate(template) 54 | 55 | assert.NilError(t, err) 56 | assert.Equal(t, result, "The secret is 'secret1'.") 57 | } 58 | 59 | func TestRenderTemplateQueryError(t *testing.T) { 60 | mockCtrl := gomock.NewController(t) 61 | defer mockCtrl.Finish() 62 | 63 | mockVaultClient := api.NewMockVaultClient(mockCtrl) 64 | 65 | mockVaultClient. 66 | EXPECT(). 67 | QuerySecret("secret/my/test/secret", "field1"). 68 | Return("", errors.New("test error")). 69 | Times(1) 70 | 71 | template := "The secret is '{{ vault \"secret/my/test/secret\" \"field1\" }}'." 72 | 73 | renderer := VaultTemplateRenderer{ 74 | vaultClient: mockVaultClient, 75 | } 76 | 77 | _, err := renderer.RenderTemplate(template) 78 | 79 | assert.Assert(t, err != nil) 80 | } 81 | 82 | func TestRenderMapTemplateQueryError(t *testing.T) { 83 | mockCtrl := gomock.NewController(t) 84 | defer mockCtrl.Finish() 85 | 86 | mockVaultClient := api.NewMockVaultClient(mockCtrl) 87 | 88 | mockVaultClient. 89 | EXPECT(). 90 | QuerySecretMap("secret/my/test/secret"). 91 | Return(nil, errors.New("test error")). 92 | Times(1) 93 | 94 | template := "The secret is '{{ vaultMap \"secret/my/test/secret\" }}'." 95 | 96 | renderer := VaultTemplateRenderer{ 97 | vaultClient: mockVaultClient, 98 | } 99 | 100 | _, err := renderer.RenderTemplate(template) 101 | 102 | assert.Assert(t, err != nil) 103 | } 104 | --------------------------------------------------------------------------------