From 6af05bb747f9fb242405788f30e6a1555c8c4c9d Mon Sep 17 00:00:00 2001 From: adohe Date: Mon, 27 Nov 2023 19:50:30 +0800 Subject: [PATCH] feat: add vault secret store impl --- go.mod | 15 + go.sum | 48 +++ pkg/apis/secrets/types.go | 13 +- pkg/secrets/providers.go | 2 + pkg/secrets/providers/hashivault/fake/fake.go | 47 +++ pkg/secrets/providers/hashivault/interface.go | 12 + pkg/secrets/providers/hashivault/vault.go | 232 ++++++++++++ .../providers/hashivault/vault_test.go | 349 ++++++++++++++++++ 8 files changed, 717 insertions(+), 1 deletion(-) create mode 100644 pkg/secrets/providers/hashivault/fake/fake.go create mode 100644 pkg/secrets/providers/hashivault/interface.go create mode 100644 pkg/secrets/providers/hashivault/vault.go create mode 100644 pkg/secrets/providers/hashivault/vault_test.go diff --git a/go.mod b/go.mod index aff793a37..f86f9abaf 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl/v2 v2.16.1 + github.com/hashicorp/vault/api v1.10.0 github.com/howieyuen/uilive v0.0.6 github.com/jinzhu/copier v0.3.2 github.com/lucasb-eyer/go-colorful v1.0.3 @@ -46,6 +47,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.4 github.com/texttheater/golang-levenshtein v1.0.1 + github.com/tidwall/gjson v1.17.0 github.com/zclconf/go-cty v1.12.1 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 @@ -84,6 +86,7 @@ require ( github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/jsonv v1.1.3 // indirect github.com/chai2010/protorpc v1.1.4 // indirect @@ -104,6 +107,7 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect @@ -127,6 +131,13 @@ require ( github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gosuri/uilive v0.0.4 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -147,6 +158,7 @@ require ( github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -172,6 +184,7 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect @@ -181,6 +194,8 @@ require ( github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/thoas/go-funk v0.9.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect diff --git a/go.sum b/go.sum index 18bcb074c..73013fad0 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,7 @@ github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible/go.mod h1:T/Aws4fEfogEE9 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-sdk-go v1.42.35 h1:N4N9buNs4YlosI9N0+WYrq8cIZwdgv34yRbxzZlTvFs= @@ -70,6 +71,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -81,6 +83,8 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytedance/mockey v1.2.4 h1:gvqdl3AqEdY3PFnne09Pn3uFGBE4qEUJH3emWOQJoi4= github.com/bytedance/mockey v1.2.4/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -144,6 +148,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -159,6 +164,8 @@ github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw4 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -240,6 +247,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -270,13 +278,34 @@ github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tf github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg= github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng= +github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= +github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/howieyuen/uilive v0.0.6 h1:BgyopdqMZNxsBiOazBlZOPtq4cl5yDLEXcRurZt25+c= @@ -344,11 +373,13 @@ github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -363,13 +394,18 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -433,6 +469,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/powerman/rpc-codec v1.2.2 h1:BK0JScZivljhwW/vLLhZLtUgqSxc/CD3sHEs8LiwwKw= github.com/powerman/rpc-codec v1.2.2/go.mod h1:3Qr/y/+u3CwcSww9tfJMRn/95lB2qUdUeIQe7BYlLDo= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.67.1 h1:u1Mw9irznvsBPxQxjUmCel1ufP3UgzA1CILj7/2tpNw= @@ -474,6 +511,9 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= @@ -520,6 +560,12 @@ github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqa github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7/go.mod h1:UxoP3EypF8JfGEjAII8jx1q8rQyDnX8qdTCs/UQBVIE= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= @@ -563,6 +609,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -621,6 +668,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/apis/secrets/types.go b/pkg/apis/secrets/types.go index 6fe3ae3fd..194bbff9b 100644 --- a/pkg/apis/secrets/types.go +++ b/pkg/apis/secrets/types.go @@ -1,5 +1,12 @@ package secrets +type VaultKVStoreVersion string + +const ( + VaultKVStoreV1 VaultKVStoreVersion = "v1" + VaultKVStoreV2 VaultKVStoreVersion = "v2" +) + // ExternalSecretRef contains information that points to the secret store data location. type ExternalSecretRef struct { // Specifies the path of the secret to read. @@ -41,5 +48,9 @@ type VaultProvider struct { Server string `yaml:"server" json:"server"` // Path is the mount path of the Vault KV backend endpoint, e.g: "secret". - Path string `yaml:"path,omitempty" json:"path,omitempty"` + Path *string `yaml:"path,omitempty" json:"path,omitempty"` + + // Version is the Vault KV secret engine version. Version can be either "v1" or + // "v2", defaults to "v2". + Version VaultKVStoreVersion `yaml:"version" json:"version"` } diff --git a/pkg/secrets/providers.go b/pkg/secrets/providers.go index 496d8ad73..2c6ad64e5 100644 --- a/pkg/secrets/providers.go +++ b/pkg/secrets/providers.go @@ -11,6 +11,8 @@ import ( "kusionstack.io/kusion/pkg/log" ) +var SecretStoreProviders = NewProviders() + type Providers struct { lock sync.RWMutex registry map[string]SecretStoreFactory diff --git a/pkg/secrets/providers/hashivault/fake/fake.go b/pkg/secrets/providers/hashivault/fake/fake.go new file mode 100644 index 000000000..471686e1b --- /dev/null +++ b/pkg/secrets/providers/hashivault/fake/fake.go @@ -0,0 +1,47 @@ +package fake + +import ( + "context" + "os" + + vault "github.com/hashicorp/vault/api" +) + +type ( + ReadWithDataWithContextFn func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) + Logical struct { + ReadWithDataWithContextFn ReadWithDataWithContextFn + } +) + +func NewReadWithContextFn(secretData map[string]interface{}, err error) ReadWithDataWithContextFn { + return func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) { + if secretData == nil { + return nil, err + } + secret := &vault.Secret{ + Data: secretData, + } + return secret, err + } +} + +func (f Logical) ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) { + return f.ReadWithDataWithContextFn(ctx, path, data) +} + +func SetTokenInEnv() func() { + oldTokenVal := os.Getenv("VAULT_SERVER_TOKEN") + os.Setenv("VAULT_SERVER_TOKEN", "fake_token") + return func() { + os.Setenv("VAULT_SERVER_TOKEN", oldTokenVal) + } +} + +func SetAlternativeTokenInEnv() func() { + oldTokenVal := os.Getenv("VAULT_TOKEN") + os.Setenv("VAULT_TOKEN", "fake_token") + return func() { + os.Setenv("VAULT_TOKEN", oldTokenVal) + } +} diff --git a/pkg/secrets/providers/hashivault/interface.go b/pkg/secrets/providers/hashivault/interface.go new file mode 100644 index 000000000..87bf71a5f --- /dev/null +++ b/pkg/secrets/providers/hashivault/interface.go @@ -0,0 +1,12 @@ +package hashivault + +import ( + "context" + + vault "github.com/hashicorp/vault/api" +) + +// Logical is a testable interface for performing logical backend operations on Vault. +type Logical interface { + ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) +} diff --git a/pkg/secrets/providers/hashivault/vault.go b/pkg/secrets/providers/hashivault/vault.go new file mode 100644 index 000000000..38288e42b --- /dev/null +++ b/pkg/secrets/providers/hashivault/vault.go @@ -0,0 +1,232 @@ +package hashivault + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strconv" + "strings" + + vault "github.com/hashicorp/vault/api" + "github.com/tidwall/gjson" + + secretsapi "kusionstack.io/kusion/pkg/apis/secrets" + "kusionstack.io/kusion/pkg/secrets" +) + +const ( + errInvalidVaultSecretStore = "cannot find valid Vault provider spec" + errReadSecret = "failed to read secret data from Vault: %w" + errParseDataField = "failed to find data field" + errJSONUnmarshall = "failed to unmarshall JSON" + errUnexpectedKey = "unexpected key in secret data: %s" + errDataPropertyFormat = "unexpected data format %s for property field: %s" + errSecretFormat = "cannot find property %s in secret data" + errBuildVaultClient = "failed to new Vault client: %w" +) + +// DefaultFactory should implement the secrets.SecretStoreFactory interface +var _ secrets.SecretStoreFactory = &DefaultFactory{} + +// vaultSecretStore should implement the secrets.SecretStore interface +var _ secrets.SecretStore = &vaultSecretStore{} + +type DefaultFactory struct{} + +func (p *DefaultFactory) Type() string { + return "Vault" +} + +// NewSecretStore constructs a Vault based secret store with specific secret store spec. +func (p *DefaultFactory) NewSecretStore(spec secretsapi.SecretStoreSpec) (secrets.SecretStore, error) { + providerSpec := spec.Provider + if providerSpec == nil || providerSpec.Vault == nil { + return nil, errors.New(errInvalidVaultSecretStore) + } + + vaultSpec := providerSpec.Vault + client, err := getVaultClient(vaultSpec.Server) + if err != nil { + return nil, err + } + + store := vaultSecretStore{ + provider: vaultSpec, + logical: client.Logical(), + } + return &store, nil +} + +func getVaultClient(server string) (*vault.Client, error) { + cfg := vault.DefaultConfig() + cfg.Address = server + c, err := vault.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf(errBuildVaultClient, err) + } + token := getVaultToken() + if token != "" { + c.SetToken(token) + } + return c, nil +} + +// getVaultToken ensures that we check both VAULT_SERVER_TOKEN and VAULT_TOKEN environment +// variables for the API token for vault. VAULT_SERVER_TOKEN takes precedence over VAULT_TOKEN. +// If neither environment variables are found, then we return an empty string as token is not required. +func getVaultToken() string { + serverToken := os.Getenv("VAULT_SERVER_TOKEN") + if serverToken != "" { + return serverToken + } + + vaultToken := os.Getenv("VAULT_TOKEN") + if vaultToken != "" { + return vaultToken + } + + return "" +} + +type vaultSecretStore struct { + provider *secretsapi.VaultProvider + logical Logical +} + +// GetSecret retrieves ref secret value from Vault server. +func (v *vaultSecretStore) GetSecret(ctx context.Context, ref secretsapi.ExternalSecretRef) ([]byte, error) { + secretData, err := v.readSecret(ctx, ref.Path, ref.Version) + if err != nil { + return nil, err + } + jsonStr, err := json.Marshal(secretData) + if err != nil { + return nil, err + } + // return raw json if no property is defined + if ref.Property == "" { + return jsonStr, nil + } + + // First try to extract key from secret with raw property + if _, ok := secretData[ref.Property]; ok { + return getTypedKey(secretData, ref.Property) + } + + // Then extract key from secret using gjson lib + val := gjson.Get(string(jsonStr), ref.Property) + if !val.Exists() { + return nil, fmt.Errorf(errSecretFormat, ref.Property) + } + return []byte(val.String()), nil +} + +func (v *vaultSecretStore) readSecret(ctx context.Context, path, version string) (map[string]interface{}, error) { + // build correct path according to vault docs for v1 and v2 API + secretPath := v.buildPath(path) + var params map[string][]string + if version != "" { + params = make(map[string][]string) + params["version"] = []string{version} + } + secret, err := v.logical.ReadWithDataWithContext(ctx, secretPath, params) + if err != nil { + return nil, fmt.Errorf(errReadSecret, err) + } + if secret == nil { + // return empty secret data + return map[string]interface{}{}, nil + } + secretData := secret.Data + if v.provider.Version == secretsapi.VaultKVStoreV2 { + // Vault KV2 has data embedded within sub-field + // Ref: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version + embeddedData, ok := secretData["data"] + if !ok { + return nil, errors.New(errParseDataField) + } + if embeddedData == nil { + // return empty secret data + return map[string]interface{}{}, nil + } + secretData, ok = embeddedData.(map[string]interface{}) + if !ok { + return nil, errors.New(errJSONUnmarshall) + } + } + return secretData, nil +} + +// buildPath is a helper method to build the final secret path. The path build logic +// varies depending on the Vault KV secrets engine version: +// v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret +// v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version +func (v *vaultSecretStore) buildPath(path string) string { + mountPath := v.provider.Path + out := path + if mountPath != nil { + prefixToCut := *mountPath + "/" + if strings.HasPrefix(out, prefixToCut) { + _, out, _ = strings.Cut(out, prefixToCut) + // if data succeeds mountPath on v2 store, we should remove it as well + if strings.HasPrefix(out, "data/") && v.provider.Version == secretsapi.VaultKVStoreV2 { + _, out, _ = strings.Cut(out, "data/") + } + } + buildPath := strings.Split(out, "/") + buildMount := strings.Split(*mountPath, "/") + if v.provider.Version == secretsapi.VaultKVStoreV2 { + buildMount = append(buildMount, "data") + } + buildMount = append(buildMount, buildPath...) + out = strings.Join(buildMount, "/") + return out + } + if !strings.Contains(out, "/data/") && v.provider.Version == secretsapi.VaultKVStoreV2 { + buildPath := strings.Split(out, "/") + buildMount := []string{buildPath[0], "data"} + buildMount = append(buildMount, buildPath[1:]...) + out = strings.Join(buildMount, "/") + return out + } + return out +} + +func getTypedKey(data map[string]interface{}, key string) ([]byte, error) { + v, ok := data[key] + if !ok { + return nil, fmt.Errorf(errUnexpectedKey, key) + } + switch t := v.(type) { + case string: + return []byte(t), nil + case map[string]interface{}: + return json.Marshal(t) + case []string: + return []byte(strings.Join(t, "\n")), nil + case []byte: + return t, nil + // also covers int and float32 due to json.Marshal + case float64: + return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil + case json.Number: + return []byte(t.String()), nil + case []interface{}: + return json.Marshal(t) + case bool: + return []byte(strconv.FormatBool(t)), nil + case nil: + return []byte(nil), nil + default: + return nil, fmt.Errorf(errDataPropertyFormat, key, reflect.TypeOf(t)) + } +} + +func init() { + secrets.SecretStoreProviders.Register(&DefaultFactory{}, &secretsapi.ProviderSpec{ + Vault: &secretsapi.VaultProvider{}, + }) +} diff --git a/pkg/secrets/providers/hashivault/vault_test.go b/pkg/secrets/providers/hashivault/vault_test.go new file mode 100644 index 000000000..27e6ff43d --- /dev/null +++ b/pkg/secrets/providers/hashivault/vault_test.go @@ -0,0 +1,349 @@ +package hashivault + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + + secretsapi "kusionstack.io/kusion/pkg/apis/secrets" + "kusionstack.io/kusion/pkg/secrets/providers/hashivault/fake" +) + +var ( + mountPath = "secret" +) + +func makeValidVaultSecretStore(v secretsapi.VaultKVStoreVersion) *vaultSecretStore { + return &vaultSecretStore{ + provider: &secretsapi.VaultProvider{ + Path: &mountPath, + Version: v, + }, + } +} + +func makeExternalSecretRef(path, property, version string) secretsapi.ExternalSecretRef { + return secretsapi.ExternalSecretRef{ + Path: path, + Property: property, + Version: version, + } +} + +func TestGetSecret(t *testing.T) { + secretData := map[string]interface{}{ + "username": "admin", + "password": "t0p-Secret", + } + secretDataWithNilValue := map[string]interface{}{ + "username": "admin", + "password": "t0p-Secret", + "token": nil, + } + secretDataWithNestedValue := map[string]interface{}{ + "username": "admin", + "password": "t0p-Secret", + "aws.accessKey": "access_key", + "gcp": map[string]string{ + "accessKey": "foo", + "secretKey": "bar", + }, + "list_of_values": []string{ + "one", + "two", + "three", + }, + "json_number": json.Number("72"), + } + secretNestedData := map[string]interface{}{ + "data": map[string]interface{}{ + "username": "admin", + "password": "t0p-Secret", + }, + } + testCases := map[string]struct { + provider *secretsapi.VaultProvider + logical Logical + path string + property string + version string + expected []byte + expectErr error + }{ + "V1_ReadSecret": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretData, nil), + }, + path: "secret/path", + property: "password", + expected: []byte(`t0p-Secret`), + expectErr: nil, + }, + "V1_ReadSecret_NoProperty": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretData, nil), + }, + path: "secret/path", + expected: []byte(`{"password":"t0p-Secret","username":"admin"}`), + expectErr: nil, + }, + "V1_ReadSecret_NoProperty_NilValue": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretDataWithNilValue, nil), + }, + path: "secret/path", + expected: []byte(`{"password":"t0p-Secret","token":null,"username":"admin"}`), + expectErr: nil, + }, + "V1_ReadSecret_NestedValue": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretDataWithNestedValue, nil), + }, + path: "secret/path", + property: "aws.accessKey", + expected: []byte(`access_key`), + expectErr: nil, + }, + "V1_ReadSecret_NestedValue_NestedProperty": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretDataWithNestedValue, nil), + }, + path: "secret/path", + property: "gcp.accessKey", + expected: []byte(`foo`), + expectErr: nil, + }, + "V1_ReadSecret_SliceValue": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretDataWithNestedValue, nil), + }, + path: "secret/path", + property: "list_of_values", + expected: []byte("one\ntwo\nthree"), + expectErr: nil, + }, + "V1_ReadSecret_JsonNumber": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretDataWithNestedValue, nil), + }, + path: "secret/path", + property: "json_number", + expected: []byte("72"), + expectErr: nil, + }, + "V1_ReadSecret_NilData": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV1).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil), + }, + path: "secret/path", + expected: []byte("{}"), + expectErr: nil, + }, + "V2_ReadSecret": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV2).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretNestedData, nil), + }, + path: "secret/path", + property: "username", + expected: []byte("admin"), + expectErr: nil, + }, + "V2_ReadSecret_WithVersion": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV2).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretNestedData, nil), + }, + path: "secret/path", + property: "username", + version: "1", + expected: []byte("admin"), + expectErr: nil, + }, + "V2_ReadSecret_ErrFormat": { + provider: makeValidVaultSecretStore(secretsapi.VaultKVStoreV2).provider, + logical: &fake.Logical{ + ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretData, nil), + }, + path: "secret/path", + expectErr: errors.New(errParseDataField), + }, + } + + for name, tc := range testCases { + store := &vaultSecretStore{ + provider: tc.provider, + logical: tc.logical, + } + ref := makeExternalSecretRef(tc.path, tc.property, tc.version) + actual, err := store.GetSecret(context.Background(), ref) + if diff := cmp.Diff(err, tc.expectErr, EquateErrors()); diff != "" { + t.Errorf("\n%s\ngot unexpected error:\n%s", name, diff) + } + if diff := cmp.Diff(string(actual), string(tc.expected)); diff != "" { + fmt.Println(diff) + t.Errorf("\n%s\nget unexpected data: \n%s", name, diff) + } + } +} + +func TestBuildPath(t *testing.T) { + storeV2 := makeValidVaultSecretStore(secretsapi.VaultKVStoreV2) + otherMountPath := "secret/path" + storeV2.provider.Path = &otherMountPath + storeV2NoPath := makeValidVaultSecretStore(secretsapi.VaultKVStoreV2) + storeV2NoPath.provider.Path = nil + + storeV1 := makeValidVaultSecretStore(secretsapi.VaultKVStoreV1) + storeV1.provider.Path = &otherMountPath + storeV1NoPath := makeValidVaultSecretStore(secretsapi.VaultKVStoreV1) + storeV1NoPath.provider.Path = nil + + testCases := map[string]struct { + store *vaultSecretStore + path string + expected string + }{ + "V2_NoMountPath_NoData": { + store: storeV2NoPath, + path: "secret/test/path", + expected: "secret/data/test/path", + }, + "V2_NoMountPath_WithData": { + store: storeV2NoPath, + path: "secret/path/data/test/first", + expected: "secret/path/data/test/first", + }, + "V2_NoData": { + store: storeV2, + path: "secret/path/test", + expected: "secret/path/data/test", + }, + "V2_WithMountPath_WithData": { + store: storeV2, + path: "secret/path/data/test", + expected: "secret/path/data/test", + }, + "V2_Simple_Key_Path": { + store: storeV2, + path: "test", + expected: "secret/path/data/test", + }, + "V1_NoMountPath": { + store: storeV1NoPath, + path: "secret/test", + expected: "secret/test", + }, + "V1_WithMountPath": { + store: storeV1, + path: "secret/path/test", + expected: "secret/path/test", + }, + } + + for name, tc := range testCases { + actual := tc.store.buildPath(tc.path) + if actual != tc.expected { + t.Errorf("%s mismatch, expected %s, actual got %s", name, tc.expected, actual) + } + } +} + +func TestNewSecretStore(t *testing.T) { + testCases := map[string]struct { + spec secretsapi.SecretStoreSpec + expectedErr error + }{ + "InvalidSecretStoreSpec": { + spec: secretsapi.SecretStoreSpec{}, + expectedErr: errors.New(errInvalidVaultSecretStore), + }, + "InvalidProviderSpec": { + spec: secretsapi.SecretStoreSpec{ + Provider: &secretsapi.ProviderSpec{}, + }, + expectedErr: errors.New(errInvalidVaultSecretStore), + }, + "ValidVaultProviderSpec": { + spec: secretsapi.SecretStoreSpec{ + Provider: &secretsapi.ProviderSpec{ + Vault: &secretsapi.VaultProvider{ + Server: "https://127.0.0.1:8200", + }, + }, + }, + expectedErr: nil, + }, + "ValidVaultProviderSpec_WithToken": { + spec: secretsapi.SecretStoreSpec{ + Provider: &secretsapi.ProviderSpec{ + Vault: &secretsapi.VaultProvider{ + Server: "https://127.0.0.1:8200", + }, + }, + }, + expectedErr: nil, + }, + } + + factory := DefaultFactory{} + for name, tc := range testCases { + _, err := factory.NewSecretStore(tc.spec) + if diff := cmp.Diff(err, tc.expectedErr, EquateErrors()); diff != "" { + t.Errorf("\n%s\ngot unexpected error:\n%s", name, diff) + } + } +} + +func TestGetVaultToken(t *testing.T) { + t.Run("Test Current Token Env Var", func(t *testing.T) { + cleanup := fake.SetTokenInEnv() + defer cleanup() + + vaultToken := getVaultToken() + if vaultToken != "fake_token" { + t.Errorf("export 'faketoken': got %q", vaultToken) + } + }) + + t.Run("Test Alternative Token Env Var", func(t *testing.T) { + cleanup := fake.SetAlternativeTokenInEnv() + defer cleanup() + + vaultToken := getVaultToken() + if vaultToken != "fake_token" { + t.Errorf("export 'faketoken': got %q", vaultToken) + } + }) +} + +// EquateErrors returns true if the supplied errors are of the same type and +// produce same error message. +func EquateErrors() cmp.Option { + return cmp.Comparer(func(a, b error) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + if av.Type() != bv.Type() { + return false + } + + return a.Error() == b.Error() + }) +}