From 5d7d54e7472cabfe925249b11f67a9f9a54f0ae3 Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Mon, 11 Dec 2023 16:31:05 +0100 Subject: [PATCH 1/7] feat(providers): Add support for fetching from k8s secrets Allows fetching values from Kubernetes secrets. Authentication to the Kubernetes cluster is managed by referencing the local kubeconfig. Signed-off-by: Bernardo Salazar --- README.md | 25 +++- go.mod | 30 ++++- go.sum | 99 +++++++++++++-- pkg/providers/k8s/k8s.go | 136 +++++++++++++++++++++ pkg/stringmapprovider/stringmapprovider.go | 3 + vals.go | 5 + 6 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 pkg/providers/k8s/k8s.go diff --git a/README.md b/README.md index 43b7a16..02a38af 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ It supports various backends including: - [Doppler](https://doppler.com/) - CredHub(Coming soon) - Pulumi State +- Kubernetes secrets - Use `vals eval -f refs.yaml` to replace all the `ref`s in the file to actual values and secrets. - Use `vals exec -f env.yaml -- ` to populate envvars and execute the command. @@ -190,7 +191,7 @@ EOF `FRAGMENT` is a path-like expression that is used to extract a single value within the secret. When a fragment is specified, `vals` parse the secret value denoted by the `PATH` into a YAML or JSON object, and traverses the object following the fragment, and uses the value at the path as the final secret value. It's supposed to be the "fragment" componet of the URI as defined in [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). -Finally, the optional trailing `+` is the explit "end" of the expression. You usually don't need it, as if omitted, it treats anything after `ref+` and before the new-line or the end-of-line as an expression to be evaluated. An explicit `+` is handy when you want to do a simple string interpolation. That is, `foo ref+SECRET1+ ref+SECRET2+ bar` evaluates to `foo SECRET1_VALUE SECRET2_VALUE bar`. +Finally, the optional trailing `+` is the explicit "end" of the expression. You usually don't need it, as if omitted, it treats anything after `ref+` and before the new-line or the end-of-line as an expression to be evaluated. An explicit `+` is handy when you want to do a simple string interpolation. That is, `foo ref+SECRET1+ ref+SECRET2+ bar` evaluates to `foo SECRET1_VALUE SECRET2_VALUE bar`. Although we mention the RFC for the sake of explanation, `PARAMS` and `FRAGMENT` might not be fully RFC-compliant as, under the hood, we use a simple regexp that seemed to work for most of use-cases. @@ -218,6 +219,7 @@ Please see the [relevant unit test cases](https://github.com/helmfile/vals/blob/ - [1Password Connect](#1password-connect) - [Doppler](#doppler) - [Pulumi State](#pulumi-state) +- [Kubernetes secrets](#kubernetes-secrets) Please see [pkg/providers](https://github.com/helmfile/vals/tree/master/pkg/providers) for the implementations of all the providers. The package names corresponds to the URI schemes. @@ -722,6 +724,27 @@ Examples: - `ref+pulumistateapi://aws-native_s3_Bucket/my-bucket/outputs/tags.%23(key==SomeKey).value?project=my-project&stack=my-stack` - `ref+pulumistateapi://kubernetes_storage.k8s.io__v1_StorageClass/gp2-encrypted/inputs/metadata.name?project=my-project&stack=my-stack` +### Kubernetes secrets + +Fetch values from Kubernetes secrets: + +- `ref+k8s://NAMESPACE/SECRET_NAME/KEY[?kubeConfigPath=&kubeContext=]` + +Authentication to the Kubernetes cluster is done by referencing the local kubeconfig file. +The path to the kubeconfig can be specified as a URI parameter, read from the `KUBECONFIG` environment variable or the provider with attempt to read `$HOME/.kube/config`. +The Kubernetes context can be specified as a URI parameteter. + +Environment variables: + +- `KUBECONFIG` contains the path to the Kubeconfig that will be used to fetch the secret. + +Examples: + +- `ref+k8s://mynamespace/mysecret/foo` +- `ref+k8s://mynamespace/mysecret/bar?kubeConfigPath=/home/user/kubeconfig` +- `secretref+k8s://mynamespace/secrets/baz` +- `secretref+k8s://mynamespace/secrets/baz?kubeContext=minikube` + ## Advanced Usages ### Discriminating config and secrets diff --git a/go.mod b/go.mod index 4406bbc..68a77fd 100644 --- a/go.mod +++ b/go.mod @@ -19,9 +19,12 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/vault/api v1.10.0 github.com/stretchr/testify v1.8.4 + github.com/tidwall/gjson v1.17.0 golang.org/x/oauth2 v0.12.0 google.golang.org/api v0.141.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 ) require ( @@ -73,14 +76,22 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/getsops/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.4.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect @@ -98,19 +109,26 @@ require ( github.com/hashicorp/go-tfe v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.11 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -120,7 +138,6 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect @@ -134,7 +151,7 @@ require ( golang.org/x/sys v0.14.0 // indirect golang.org/x/term v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect @@ -143,5 +160,14 @@ require ( google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/gookit/color.v1 v1.1.6 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/api v0.28.4 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index b215965..9b8656e 100644 --- a/go.sum +++ b/go.sum @@ -128,7 +128,6 @@ github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4r github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= @@ -137,6 +136,7 @@ github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvA github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -154,6 +154,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -169,6 +171,17 @@ github.com/getsops/sops/v3 v3.8.0 h1:jMbaxVKxGDGp36DejvXvqWDh2vRJmUSDHKWNcHNYfZE github.com/getsops/sops/v3 v3.8.0/go.mod h1:1t7vEMUbtBzLfhKQXHYQg7TYOAcBLLZNHGuAV6EJtz0= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -184,7 +197,6 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -198,6 +210,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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= @@ -211,8 +225,13 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -277,15 +296,27 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -309,8 +340,17 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= +github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= @@ -329,6 +369,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +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= @@ -371,6 +413,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= @@ -378,6 +422,7 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -389,7 +434,8 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= @@ -401,6 +447,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -417,6 +465,8 @@ golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= @@ -462,20 +512,24 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.141.0 h1:Df6vfMgDoIM6ss0m7H4MPwFwY87WNXHfBIda/Bmfl4E= @@ -514,13 +568,16 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/gookit/color.v1 v1.1.6 h1:5fB10p6AUFjhd2ayq9JgmJWr9WlTrguFdw3qlYtKNHk= gopkg.in/gookit/color.v1 v1.1.6/go.mod h1:IcEkFGaveVShJ+j8ew+jwe9epHyGpJ9IrptHmW3laVY= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -528,3 +585,21 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go new file mode 100644 index 0000000..2c1bef7 --- /dev/null +++ b/pkg/providers/k8s/k8s.go @@ -0,0 +1,136 @@ +package k8s + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/helmfile/vals/pkg/api" + "github.com/helmfile/vals/pkg/log" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type provider struct { + log *log.Logger + KubeConfigPath string + KubeContext string +} + +func New(l *log.Logger, cfg api.StaticConfig) *provider { + p := &provider{ + log: l, + } + + kubeConfig, err := getKubeConfig(cfg, *l) + if err != nil { + fmt.Printf("An error occurred getting the Kubeconfig path: %s", err) + } + + p.KubeConfigPath = kubeConfig + p.KubeContext = getKubeContext(cfg, *l) + + return p +} + +func getKubeConfig(cfg api.StaticConfig, logger log.Logger) (string, error) { + // Use kubeConfigPath from URI parameters if specified + if cfg.String("kubeConfigPath") != "" { + return cfg.String("kubeConfigPath"), nil + } + + // Use path in KUBECONFIG environment variable if set + if envPath := os.Getenv("KUBECONFIG"); envPath != "" { + return envPath, nil + } + + // Use default kubeconfig path if it exists + homeDir, err := os.UserHomeDir() + + if err != nil { + return "", fmt.Errorf("An error occurred getting the user's home directory: %s", err) + } + + defaultPath := homeDir + "/.kube/config" + if _, err := os.Stat(defaultPath); err == nil { + return defaultPath, nil + } + + return "", fmt.Errorf("No Kubeconfig path was provided. Please provide a path to a Kubeconfig file using the 'kubeConfigPath' key in your config file.") +} + +func (p *provider) GetString(path string) (string, error) { + separator := "/" + splits := strings.Split(path, separator) + + if len(splits) != 3 { + return "", fmt.Errorf("Invalid path %s. Path must be in the format //", path) + } + + namespace := splits[0] + secretName := splits[1] + key := splits[2] + + secretData, err := getSecret(namespace, secretName, p.KubeConfigPath, p.KubeContext, context.Background()) + secret, exists := secretData[key] + + if err != nil || !exists { + err := fmt.Errorf("Key %s does not exist in %s/%s", key, secretName, namespace) + return "", err + } + + p.log.Debugf("vals-k8s: Retrieved secret %s/%s/%s (KubeContext: %s)", namespace, secretName, key, p.KubeContext) + + return string(secret), nil +} + +func (p *provider) GetStringMap(path string) (map[string]interface{}, error) { + return nil, fmt.Errorf("This provider does not support values from URI fragments") +} + +// Return an empty Kube context if none is provided +func getKubeContext(cfg api.StaticConfig, logger log.Logger) string { + if cfg.String("kubeContext") != "" { + return cfg.String("kubeContext") + } else { + return "" + } +} + +// Build the client-go config using a specific context +func buildConfigWithContextFromFlags(context string, kubeconfigPath string) (*rest.Config, error) { + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, + &clientcmd.ConfigOverrides{ + CurrentContext: context, + }).ClientConfig() +} + +// Fetch the secret from the Kubernetes cluster +func getSecret(namespace string, secretName string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string][]byte, error) { + if kubeContext == "" { + fmt.Printf("vals-k8s: kubeContext was not provided. Using current context.") + } + + config, err := buildConfigWithContextFromFlags(kubeContext, kubeConfigPath) + + if err != nil { + return nil, fmt.Errorf("Unable to build Kubeconfig from vals configuration: %s", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("Unable to create the Kubernetes client: %s", err) + } + + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("Unable to get the secret from Kubernetes: %s", err) + } + + return secret.Data, nil +} diff --git a/pkg/stringmapprovider/stringmapprovider.go b/pkg/stringmapprovider/stringmapprovider.go index 04d725d..d0923a6 100644 --- a/pkg/stringmapprovider/stringmapprovider.go +++ b/pkg/stringmapprovider/stringmapprovider.go @@ -11,6 +11,7 @@ import ( "github.com/helmfile/vals/pkg/providers/doppler" "github.com/helmfile/vals/pkg/providers/gcpsecrets" "github.com/helmfile/vals/pkg/providers/gkms" + "github.com/helmfile/vals/pkg/providers/k8s" "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/sops" "github.com/helmfile/vals/pkg/providers/ssm" @@ -43,6 +44,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringMapProvi return doppler.New(l, provider), nil case "gkms": return gkms.New(l, provider), nil + case "k8s": + return k8s.New(l, provider), nil } return nil, fmt.Errorf("failed initializing string-map provider from config: %v", provider) diff --git a/vals.go b/vals.go index 1e09712..c5b4f2d 100644 --- a/vals.go +++ b/vals.go @@ -32,6 +32,7 @@ import ( "github.com/helmfile/vals/pkg/providers/gitlab" "github.com/helmfile/vals/pkg/providers/gkms" "github.com/helmfile/vals/pkg/providers/googlesheets" + "github.com/helmfile/vals/pkg/providers/k8s" "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/pulumi" "github.com/helmfile/vals/pkg/providers/s3" @@ -89,6 +90,7 @@ const ( ProviderDoppler = "doppler" ProviderPulumiStateAPI = "pulumistateapi" ProviderGKMS = "gkms" + ProviderK8s = "k8s" ) var ( @@ -248,6 +250,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { case ProviderGKMS: p := gkms.New(r.logger, conf) return p, nil + case ProviderK8s: + p := k8s.New(r.logger, conf) + return p, nil } return nil, fmt.Errorf("no provider registered for scheme %q", scheme) } From ea637925bc42b49582d6143fc61a1108c0b9a3f3 Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Tue, 12 Dec 2023 12:43:39 +0100 Subject: [PATCH 2/7] fix(lint): fix linting and error handling Signed-off-by: Bernardo Salazar --- pkg/providers/k8s/k8s.go | 45 +++++++++++++++++----------- pkg/stringprovider/stringprovider.go | 3 ++ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index 2c1bef7..cfcd27d 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -6,13 +6,13 @@ import ( "os" "strings" - "github.com/helmfile/vals/pkg/api" - "github.com/helmfile/vals/pkg/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + + "github.com/helmfile/vals/pkg/api" + "github.com/helmfile/vals/pkg/log" ) type provider struct { @@ -26,31 +26,35 @@ func New(l *log.Logger, cfg api.StaticConfig) *provider { log: l, } - kubeConfig, err := getKubeConfig(cfg, *l) + kubeConfig, err := getKubeConfig(cfg) if err != nil { - fmt.Printf("An error occurred getting the Kubeconfig path: %s", err) + fmt.Printf("An error occurred getting the Kubeconfig path: %s\n", err) + return p } p.KubeConfigPath = kubeConfig - p.KubeContext = getKubeContext(cfg, *l) + p.KubeContext = getKubeContext(cfg) return p } -func getKubeConfig(cfg api.StaticConfig, logger log.Logger) (string, error) { +func getKubeConfig(cfg api.StaticConfig) (string, error) { // Use kubeConfigPath from URI parameters if specified if cfg.String("kubeConfigPath") != "" { - return cfg.String("kubeConfigPath"), nil + if _, err := os.Stat(cfg.String("kubeConfigPath")); err != nil { + return cfg.String("kubeConfigPath"), fmt.Errorf("kubeConfigPath URI parameter is set but path %s does not exist.", cfg.String("kubeConfigPath")) + } } // Use path in KUBECONFIG environment variable if set if envPath := os.Getenv("KUBECONFIG"); envPath != "" { - return envPath, nil + if _, err := os.Stat(envPath); err != nil { + return envPath, fmt.Errorf("KUBECONFIG environment variable is set but path %s does not exist.", envPath) + } } // Use default kubeconfig path if it exists homeDir, err := os.UserHomeDir() - if err != nil { return "", fmt.Errorf("An error occurred getting the user's home directory: %s", err) } @@ -60,7 +64,7 @@ func getKubeConfig(cfg api.StaticConfig, logger log.Logger) (string, error) { return defaultPath, nil } - return "", fmt.Errorf("No Kubeconfig path was provided. Please provide a path to a Kubeconfig file using the 'kubeConfigPath' key in your config file.") + return "", fmt.Errorf("No path was found in any of the following: kubeContext URI param, KUBECONFIG environment variable, or default path %s does not exist.", defaultPath) } func (p *provider) GetString(path string) (string, error) { @@ -75,15 +79,22 @@ func (p *provider) GetString(path string) (string, error) { secretName := splits[1] key := splits[2] + if p.KubeConfigPath == "" { + return "", fmt.Errorf("No Kubeconfig path was found") + } + secretData, err := getSecret(namespace, secretName, p.KubeConfigPath, p.KubeContext, context.Background()) secret, exists := secretData[key] - if err != nil || !exists { - err := fmt.Errorf("Key %s does not exist in %s/%s", key, secretName, namespace) - return "", err + return "", fmt.Errorf("Key %s does not exist in %s/%s", key, namespace, secretName) } - p.log.Debugf("vals-k8s: Retrieved secret %s/%s/%s (KubeContext: %s)", namespace, secretName, key, p.KubeContext) + // Print success message with kubeContext if provided + message := fmt.Sprintf("vals-k8s: Retrieved secret %s/%s/%s", namespace, secretName, key) + if p.KubeContext != "" { + message += fmt.Sprintf(" (KubeContext: %s)", p.KubeContext) + } + p.log.Debugf(message) return string(secret), nil } @@ -93,7 +104,7 @@ func (p *provider) GetStringMap(path string) (map[string]interface{}, error) { } // Return an empty Kube context if none is provided -func getKubeContext(cfg api.StaticConfig, logger log.Logger) string { +func getKubeContext(cfg api.StaticConfig) string { if cfg.String("kubeContext") != "" { return cfg.String("kubeContext") } else { @@ -113,7 +124,7 @@ func buildConfigWithContextFromFlags(context string, kubeconfigPath string) (*re // Fetch the secret from the Kubernetes cluster func getSecret(namespace string, secretName string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string][]byte, error) { if kubeContext == "" { - fmt.Printf("vals-k8s: kubeContext was not provided. Using current context.") + fmt.Printf("vals-k8s: kubeContext was not provided. Using current context.\n") } config, err := buildConfigWithContextFromFlags(kubeContext, kubeConfigPath) diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index 456111f..8a0a35b 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -13,6 +13,7 @@ import ( "github.com/helmfile/vals/pkg/providers/gcs" "github.com/helmfile/vals/pkg/providers/gitlab" "github.com/helmfile/vals/pkg/providers/gkms" + "github.com/helmfile/vals/pkg/providers/k8s" "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/pulumi" "github.com/helmfile/vals/pkg/providers/s3" @@ -64,6 +65,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringProvider return pulumi.New(l, provider, "pulumistateapi"), nil case "gkms": return gkms.New(l, provider), nil + case "k8s": + return k8s.New(l, provider), nil } return nil, fmt.Errorf("failed initializing string provider from config: %v", provider) From 4616e3d2c77e30d118dbb357d3eb4e56c5186be6 Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Tue, 12 Dec 2023 17:21:39 +0100 Subject: [PATCH 3/7] chore(test): add a test, improve error handling Signed-off-by: Bernardo Salazar --- README.md | 4 +- pkg/providers/k8s/k8s.go | 16 ++--- pkg/stringmapprovider/stringmapprovider.go | 2 +- pkg/stringprovider/stringprovider.go | 2 +- vals.go | 3 +- vals_k8s_test.go | 69 ++++++++++++++++++++++ 6 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 vals_k8s_test.go diff --git a/README.md b/README.md index 02a38af..24009cc 100644 --- a/README.md +++ b/README.md @@ -731,7 +731,7 @@ Fetch values from Kubernetes secrets: - `ref+k8s://NAMESPACE/SECRET_NAME/KEY[?kubeConfigPath=&kubeContext=]` Authentication to the Kubernetes cluster is done by referencing the local kubeconfig file. -The path to the kubeconfig can be specified as a URI parameter, read from the `KUBECONFIG` environment variable or the provider with attempt to read `$HOME/.kube/config`. +The path to the kubeconfig can be specified as a URI parameter, read from the `KUBECONFIG` environment variable or the provider will attempt to read `$HOME/.kube/config`. The Kubernetes context can be specified as a URI parameteter. Environment variables: @@ -792,7 +792,7 @@ That's not the business of vals. Instead, use vals solely for composing sets of values that are then input to another templating engine or data manipulation language like Jsonnet and CUE. -Note though, `vals` dose have support for simple string interpolation like usage. See [Expression Syntax](#expression-syntax) for more information. +Note though, `vals` does have support for simple string interpolation like usage. See [Expression Syntax](#expression-syntax) for more information. ### Merge diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index cfcd27d..d588cf3 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -21,21 +21,21 @@ type provider struct { KubeContext string } -func New(l *log.Logger, cfg api.StaticConfig) *provider { +func New(l *log.Logger, cfg api.StaticConfig) (*provider, error) { p := &provider{ log: l, } kubeConfig, err := getKubeConfig(cfg) if err != nil { - fmt.Printf("An error occurred getting the Kubeconfig path: %s\n", err) - return p + fmt.Printf("Unable to get a valid Kubeconfig path: %s\n", err) + return nil, err } p.KubeConfigPath = kubeConfig p.KubeContext = getKubeContext(cfg) - return p + return p, nil } func getKubeConfig(cfg api.StaticConfig) (string, error) { @@ -79,13 +79,13 @@ func (p *provider) GetString(path string) (string, error) { secretName := splits[1] key := splits[2] - if p.KubeConfigPath == "" { - return "", fmt.Errorf("No Kubeconfig path was found") + secretData, err := getSecret(namespace, secretName, p.KubeConfigPath, p.KubeContext, context.Background()) + if err != nil { + return "", fmt.Errorf("Unable to get secret %s/%s: %s", namespace, secretName, err) } - secretData, err := getSecret(namespace, secretName, p.KubeConfigPath, p.KubeContext, context.Background()) secret, exists := secretData[key] - if err != nil || !exists { + if !exists { return "", fmt.Errorf("Key %s does not exist in %s/%s", key, namespace, secretName) } diff --git a/pkg/stringmapprovider/stringmapprovider.go b/pkg/stringmapprovider/stringmapprovider.go index d0923a6..28e2a39 100644 --- a/pkg/stringmapprovider/stringmapprovider.go +++ b/pkg/stringmapprovider/stringmapprovider.go @@ -45,7 +45,7 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringMapProvi case "gkms": return gkms.New(l, provider), nil case "k8s": - return k8s.New(l, provider), nil + return k8s.New(l, provider) } return nil, fmt.Errorf("failed initializing string-map provider from config: %v", provider) diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index 8a0a35b..cb8e0af 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -66,7 +66,7 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringProvider case "gkms": return gkms.New(l, provider), nil case "k8s": - return k8s.New(l, provider), nil + return k8s.New(l, provider) } return nil, fmt.Errorf("failed initializing string provider from config: %v", provider) diff --git a/vals.go b/vals.go index c5b4f2d..8d48df2 100644 --- a/vals.go +++ b/vals.go @@ -251,8 +251,7 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { p := gkms.New(r.logger, conf) return p, nil case ProviderK8s: - p := k8s.New(r.logger, conf) - return p, nil + return k8s.New(r.logger, conf) } return nil, fmt.Errorf("no provider registered for scheme %q", scheme) } diff --git a/vals_k8s_test.go b/vals_k8s_test.go new file mode 100644 index 0000000..60b7ea0 --- /dev/null +++ b/vals_k8s_test.go @@ -0,0 +1,69 @@ +package vals + +import ( + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestValues_k8s(t *testing.T) { + // Setup: + // create a local Kubernetes cluster using minikube: + // minikube start + // create a namespace: + // kubectl create namespace test-namespace + // create a secret: + // kubectl create secret generic mysecret -n test-namespace --from-literal=key=p4ssw0rd + + type testcase struct { + template map[string]interface{} + expected map[string]interface{} + } + + namespace := "test-namespace" + key := "key" + homeDir, _ := os.UserHomeDir() + + testcases := []testcase{ + { + template: map[string]interface{}{ + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s", namespace, "mysecret", key), + }, + expected: map[string]interface{}{ + "test_key": "p4ssw0rd", + }, + }, + { + template: map[string]interface{}{ + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s?kubeContext=minikube", namespace, "mysecret", key), + }, + expected: map[string]interface{}{ + "test_key": "p4ssw0rd", + }, + }, + { + template: map[string]interface{}{ + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s?kubeContext=minikube&kubeConfigPath=%s/.kube/config", namespace, "mysecret", key, homeDir), + }, + expected: map[string]interface{}{ + "test_key": "p4ssw0rd", + }, + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + vals, err := Eval(tc.template) + if err != nil { + t.Fatalf("%v", err) + } + diff := cmp.Diff(tc.expected, vals) + if diff != "" { + t.Errorf("unexpected diff: %s", diff) + } + }) + } +} From c0f7f64f1312b08ac2f981a9b74487b3080c86d3 Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Wed, 13 Dec 2023 18:11:04 +0100 Subject: [PATCH 4/7] feat(k8s): update secret API reference Updates the reference to allow other Kubernetes objects like ConfigMaps, which can become a valid use case. Also adds unit tests. Signed-off-by: Bernardo Salazar --- README.md | 14 ++- pkg/providers/k8s/k8s.go | 50 ++++---- pkg/providers/k8s/k8s_test.go | 209 ++++++++++++++++++++++++++++++++++ vals_k8s_test.go | 37 ++++-- 4 files changed, 276 insertions(+), 34 deletions(-) create mode 100644 pkg/providers/k8s/k8s_test.go diff --git a/README.md b/README.md index 24009cc..fe58b96 100644 --- a/README.md +++ b/README.md @@ -726,9 +726,9 @@ Examples: ### Kubernetes secrets -Fetch values from Kubernetes secrets: +Fetch value from a Kubernetes secret: -- `ref+k8s://NAMESPACE/SECRET_NAME/KEY[?kubeConfigPath=&kubeContext=]` +- `ref+k8s://API_VERSION/KIND/NAMESPACE/NAME/KEY[?kubeConfigPath=&kubeContext=]` Authentication to the Kubernetes cluster is done by referencing the local kubeconfig file. The path to the kubeconfig can be specified as a URI parameter, read from the `KUBECONFIG` environment variable or the provider will attempt to read `$HOME/.kube/config`. @@ -740,10 +740,12 @@ Environment variables: Examples: -- `ref+k8s://mynamespace/mysecret/foo` -- `ref+k8s://mynamespace/mysecret/bar?kubeConfigPath=/home/user/kubeconfig` -- `secretref+k8s://mynamespace/secrets/baz` -- `secretref+k8s://mynamespace/secrets/baz?kubeContext=minikube` +- `ref+k8s://v1/Secret/mynamespace/mysecret/foo` +- `ref+k8s://v1/Secret/mynamespace/mysecret/bar?kubeConfigPath=/home/user/kubeconfig` +- `secretref+k8s://v1/Secret/mynamespace/secrets/baz` +- `secretref+k8s://v1/Secret/mynamespace/secrets/baz?kubeContext=minikube` + +> NOTE: This provider only supports kind "Secret" in apiVersion "v1" at this time. ## Advanced Usages diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index d588cf3..eb338b7 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -28,7 +28,7 @@ func New(l *log.Logger, cfg api.StaticConfig) (*provider, error) { kubeConfig, err := getKubeConfig(cfg) if err != nil { - fmt.Printf("Unable to get a valid Kubeconfig path: %s\n", err) + p.log.Debugf("Unable to get a valid Kubeconfig path: %s\n", err) return nil, err } @@ -42,14 +42,14 @@ func getKubeConfig(cfg api.StaticConfig) (string, error) { // Use kubeConfigPath from URI parameters if specified if cfg.String("kubeConfigPath") != "" { if _, err := os.Stat(cfg.String("kubeConfigPath")); err != nil { - return cfg.String("kubeConfigPath"), fmt.Errorf("kubeConfigPath URI parameter is set but path %s does not exist.", cfg.String("kubeConfigPath")) + return "", fmt.Errorf("kubeConfigPath URI parameter is set but path %s does not exist.", cfg.String("kubeConfigPath")) } } // Use path in KUBECONFIG environment variable if set if envPath := os.Getenv("KUBECONFIG"); envPath != "" { if _, err := os.Stat(envPath); err != nil { - return envPath, fmt.Errorf("KUBECONFIG environment variable is set but path %s does not exist.", envPath) + return "", fmt.Errorf("KUBECONFIG environment variable is set but path %s does not exist.", envPath) } } @@ -71,32 +71,44 @@ func (p *provider) GetString(path string) (string, error) { separator := "/" splits := strings.Split(path, separator) - if len(splits) != 3 { - return "", fmt.Errorf("Invalid path %s. Path must be in the format //", path) + if len(splits) != 5 { + return "", fmt.Errorf("Invalid path %s. Path must be in the format ////", path) } - namespace := splits[0] - secretName := splits[1] - key := splits[2] + apiVersion := splits[0] + kind := splits[1] + namespace := splits[2] + name := splits[3] + key := splits[4] - secretData, err := getSecret(namespace, secretName, p.KubeConfigPath, p.KubeContext, context.Background()) + if apiVersion != "v1" { + return "", fmt.Errorf("Invalid apiVersion %s. Only apiVersion v1 is supported at this time.", apiVersion) + } + if kind != "Secret" { + return "", fmt.Errorf("Invalid kind %s. Only kind Secret is supported at this time.", kind) + } + + //TODO: + // At this time, only Secret kind with v1 apiVersion version is supported. + // getObject() should be extended to support both ConfigMap and Secrets kind in other apiVersions. + objectData, err := getObject(namespace, name, p.KubeConfigPath, p.KubeContext, context.Background()) if err != nil { - return "", fmt.Errorf("Unable to get secret %s/%s: %s", namespace, secretName, err) + return "", fmt.Errorf("Unable to get %s %s/%s: %s", kind, namespace, name, err) } - secret, exists := secretData[key] + object, exists := objectData[key] if !exists { - return "", fmt.Errorf("Key %s does not exist in %s/%s", key, namespace, secretName) + return "", fmt.Errorf("Key %s does not exist in %s/%s", key, namespace, name) } // Print success message with kubeContext if provided - message := fmt.Sprintf("vals-k8s: Retrieved secret %s/%s/%s", namespace, secretName, key) + message := fmt.Sprintf("vals-k8s: Retrieved %s: %s/%s/%s", kind, namespace, name, key) if p.KubeContext != "" { message += fmt.Sprintf(" (KubeContext: %s)", p.KubeContext) } p.log.Debugf(message) - return string(secret), nil + return string(object), nil } func (p *provider) GetStringMap(path string) (map[string]interface{}, error) { @@ -121,8 +133,8 @@ func buildConfigWithContextFromFlags(context string, kubeconfigPath string) (*re }).ClientConfig() } -// Fetch the secret from the Kubernetes cluster -func getSecret(namespace string, secretName string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string][]byte, error) { +// Fetch the object from the Kubernetes cluster +func getObject(namespace string, name string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string][]byte, error) { if kubeContext == "" { fmt.Printf("vals-k8s: kubeContext was not provided. Using current context.\n") } @@ -138,10 +150,10 @@ func getSecret(namespace string, secretName string, kubeConfigPath string, kubeC return nil, fmt.Errorf("Unable to create the Kubernetes client: %s", err) } - secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + object, err := clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return nil, fmt.Errorf("Unable to get the secret from Kubernetes: %s", err) + return nil, fmt.Errorf("Unable to get the object from Kubernetes: %s", err) } - return secret.Data, nil + return object.Data, nil } diff --git a/pkg/providers/k8s/k8s_test.go b/pkg/providers/k8s/k8s_test.go new file mode 100644 index 0000000..ec16d4d --- /dev/null +++ b/pkg/providers/k8s/k8s_test.go @@ -0,0 +1,209 @@ +package k8s + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/helmfile/vals/pkg/config" + "github.com/helmfile/vals/pkg/log" +) + +// Setup: +// create a local Kubernetes cluster using minikube: +// minikube start +// create a namespace: +// kubectl create namespace test-namespace +// create a secret: +// kubectl create secret generic mysecret -n test-namespace --from-literal=key=p4ssw0rd + +func Test_getObject(t *testing.T) { + homeDir, _ := os.UserHomeDir() + testcases := []struct { + namespace string + name string + kubeConfigPath string + want map[string][]uint8 + wantErr string + }{ + { + namespace: "test-namespace", + name: "mysecret", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: map[string][]uint8{"key": []uint8("p4ssw0rd")}, + wantErr: "", + }, + // kubeConfigPath does not exist + { + namespace: "test-namespace", + name: "mysecret", + kubeConfigPath: "/tmp/does-not-exist", + want: nil, + wantErr: "Unable to build Kubeconfig from vals configuration: stat /tmp/does-not-exist: no such file or directory", + }, + // namespace does not exist + { + namespace: "non-existent-namespace", + name: "mysecret", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: nil, + wantErr: "Unable to get the object from Kubernetes: secrets \"mysecret\" not found", + }, + // secret does not exist + { + namespace: "test-namespace", + name: "non-existent-secret", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: nil, + wantErr: "Unable to get the object from Kubernetes: secrets \"non-existent-secret\" not found", + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + got, err := getObject(tc.namespace, tc.name, tc.kubeConfigPath, "", context.Background()) + if err != nil { + if err.Error() != tc.wantErr { + t.Fatalf("unexpected error: want %q, got %q", tc.wantErr, err.Error()) + } + } else { + if tc.wantErr != "" { + t.Fatalf("expected error did not occur: want %q, got none", tc.wantErr) + } + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected result: -(want), +(got)\n%s", diff) + } + }) + } +} + +func Test_getKubeConfig(t *testing.T) { + homeDir, _ := os.UserHomeDir() + testcases := []struct { + config config.MapConfig + want string + wantErr string + }{ + { + config: config.MapConfig{ + M: map[string]interface{}{ + "kubeConfigPath": fmt.Sprintf("%s/.kube/config", homeDir), + }, + }, + want: fmt.Sprintf("%s/.kube/config", homeDir), + wantErr: "", + }, + // kubeConfigPath does not exist + { + config: config.MapConfig{ + M: map[string]interface{}{"kubeConfigPath": "/tmp/does-not-exist"}, + }, + want: "", + wantErr: "kubeConfigPath URI parameter is set but path /tmp/does-not-exist does not exist.", + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + got, err := getKubeConfig(tc.config) + if err != nil { + if err.Error() != tc.wantErr { + t.Fatalf("unexpected error: want %q, got %q", tc.wantErr, err.Error()) + } + } else { + if tc.wantErr != "" { + t.Fatalf("expected error did not occur: want %q, got none", tc.wantErr) + } + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected result: -(want), +(got)\n%s", diff) + } + }) + } + +} +func Test_getKubeContext(t *testing.T) { + testcases := []struct { + config config.MapConfig + want string + }{ + { + config: config.MapConfig{ + M: map[string]interface{}{ + "kubeContext": "minikube", + }, + }, + want: "minikube", + }, + // kubeContext is not specified, should return empty + { + config: config.MapConfig{ + M: map[string]interface{}{"kubeConfigPath": ""}, + }, + want: "", + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + got := getKubeContext(tc.config) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected result: -(want), +(got)\n%s", diff) + } + }) + } +} + +func Test_GetString(t *testing.T) { + logger := log.New(log.Config{Output: os.Stderr}) + tests := []struct { + path string + want string + wantErr string + }{ + { + path: "v1/Secret/test-namespace/mysecret/key", + want: "p4ssw0rd", + wantErr: "", + }, + { + path: "v1/Secret/test-namespace/mysecret/non-existent-key", + want: "", + wantErr: "Key non-existent-key does not exist in test-namespace/mysecret", + }, + } + for _, tc := range tests { + t.Run(tc.path, func(t *testing.T) { + // Create provider with mock + homeDir, _ := os.UserHomeDir() + conf := map[string]interface{}{} + conf["kubeConfigPath"] = fmt.Sprintf("%s/.kube/config", homeDir) + conf["kubeContext"] = "minikube" + p, _ := New(logger, config.MapConfig{M: conf}) + + got, err := p.GetString(tc.path) + if err != nil { + if err.Error() != tc.wantErr { + t.Fatalf("unexpected error: want %q, got %q", tc.wantErr, err.Error()) + } + } else { + if tc.wantErr != "" { + t.Fatalf("expected error did not occur: want %q, got none", tc.wantErr) + } + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected result: -(want), +(got)\n%s", diff) + } + }) + } +} diff --git a/vals_k8s_test.go b/vals_k8s_test.go index 60b7ea0..7bff2e4 100644 --- a/vals_k8s_test.go +++ b/vals_k8s_test.go @@ -19,9 +19,12 @@ func TestValues_k8s(t *testing.T) { type testcase struct { template map[string]interface{} - expected map[string]interface{} + want map[string]interface{} + wantErr string } + apiVersion := "v1" + kind := "Secret" namespace := "test-namespace" key := "key" homeDir, _ := os.UserHomeDir() @@ -29,27 +32,37 @@ func TestValues_k8s(t *testing.T) { testcases := []testcase{ { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s", namespace, "mysecret", key), + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s", apiVersion, kind, namespace, "mysecret", key), }, - expected: map[string]interface{}{ + want: map[string]interface{}{ "test_key": "p4ssw0rd", }, + wantErr: "", }, { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s?kubeContext=minikube", namespace, "mysecret", key), + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s?kubeContext=minikube", apiVersion, kind, namespace, "mysecret", key), }, - expected: map[string]interface{}{ + want: map[string]interface{}{ "test_key": "p4ssw0rd", }, + wantErr: "", }, { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s?kubeContext=minikube&kubeConfigPath=%s/.kube/config", namespace, "mysecret", key, homeDir), + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s?kubeContext=minikube&kubeConfigPath=%s/.kube/config", apiVersion, kind, namespace, "mysecret", key, homeDir), }, - expected: map[string]interface{}{ + want: map[string]interface{}{ "test_key": "p4ssw0rd", }, + wantErr: "", + }, + { + template: map[string]interface{}{ + "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s?kubeContext=minikube&kubeConfigPath=%s/.kube/config", "v2", kind, namespace, "mysecret", key, homeDir), + }, + want: nil, + wantErr: fmt.Sprintf("expand k8s://v2/Secret/test-namespace/mysecret/key?kubeContext=minikube&kubeConfigPath=%s/.kube/config: Invalid apiVersion v2. Only apiVersion v1 is supported at this time.", homeDir), }, } @@ -58,9 +71,15 @@ func TestValues_k8s(t *testing.T) { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { vals, err := Eval(tc.template) if err != nil { - t.Fatalf("%v", err) + if err.Error() != tc.wantErr { + t.Fatalf("unexpected error: want %q, got %q", tc.wantErr, err.Error()) + } + } else { + if tc.wantErr != "" { + t.Fatalf("expected error did not occur: want %q, got none", tc.wantErr) + } } - diff := cmp.Diff(tc.expected, vals) + diff := cmp.Diff(tc.want, vals) if diff != "" { t.Errorf("unexpected diff: %s", diff) } From a1dff61255c61641853cea7e5905d48227185d3a Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Thu, 14 Dec 2023 09:43:07 +0100 Subject: [PATCH 5/7] fix(lint): minor lint and doc update Signed-off-by: Bernardo Salazar --- README.md | 4 ++-- pkg/providers/k8s/k8s.go | 3 +-- pkg/providers/k8s/k8s_test.go | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe58b96..6939be2 100644 --- a/README.md +++ b/README.md @@ -742,8 +742,8 @@ Examples: - `ref+k8s://v1/Secret/mynamespace/mysecret/foo` - `ref+k8s://v1/Secret/mynamespace/mysecret/bar?kubeConfigPath=/home/user/kubeconfig` -- `secretref+k8s://v1/Secret/mynamespace/secrets/baz` -- `secretref+k8s://v1/Secret/mynamespace/secrets/baz?kubeContext=minikube` +- `secretref+k8s://v1/Secret/mynamespace/mysecret/baz` +- `secretref+k8s://v1/Secret/mynamespace/mysecret/baz?kubeContext=minikube` > NOTE: This provider only supports kind "Secret" in apiVersion "v1" at this time. diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index eb338b7..cf6903a 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -119,9 +119,8 @@ func (p *provider) GetStringMap(path string) (map[string]interface{}, error) { func getKubeContext(cfg api.StaticConfig) string { if cfg.String("kubeContext") != "" { return cfg.String("kubeContext") - } else { - return "" } + return "" } // Build the client-go config using a specific context diff --git a/pkg/providers/k8s/k8s_test.go b/pkg/providers/k8s/k8s_test.go index ec16d4d..240994f 100644 --- a/pkg/providers/k8s/k8s_test.go +++ b/pkg/providers/k8s/k8s_test.go @@ -128,7 +128,6 @@ func Test_getKubeConfig(t *testing.T) { } }) } - } func Test_getKubeContext(t *testing.T) { testcases := []struct { From 7b6a295d2851e1a1cc5346a2657dad5517502c9a Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Thu, 14 Dec 2023 13:16:55 +0100 Subject: [PATCH 6/7] fix: return values for kubeConfigPath and env var Signed-off-by: Bernardo Salazar --- pkg/providers/k8s/k8s.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index cf6903a..e923f09 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -44,6 +44,7 @@ func getKubeConfig(cfg api.StaticConfig) (string, error) { if _, err := os.Stat(cfg.String("kubeConfigPath")); err != nil { return "", fmt.Errorf("kubeConfigPath URI parameter is set but path %s does not exist.", cfg.String("kubeConfigPath")) } + return cfg.String("kubeConfigPath"), nil } // Use path in KUBECONFIG environment variable if set @@ -51,6 +52,7 @@ func getKubeConfig(cfg api.StaticConfig) (string, error) { if _, err := os.Stat(envPath); err != nil { return "", fmt.Errorf("KUBECONFIG environment variable is set but path %s does not exist.", envPath) } + return envPath, nil } // Use default kubeconfig path if it exists From 003edf4f1c925fff563183349358ad8be758d661 Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Thu, 14 Dec 2023 18:38:49 +0100 Subject: [PATCH 7/7] feat(k8s): more test coverage Signed-off-by: Bernardo Salazar --- pkg/providers/k8s/k8s_test.go | 80 +++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/pkg/providers/k8s/k8s_test.go b/pkg/providers/k8s/k8s_test.go index 240994f..eb1fb5d 100644 --- a/pkg/providers/k8s/k8s_test.go +++ b/pkg/providers/k8s/k8s_test.go @@ -29,6 +29,7 @@ func Test_getObject(t *testing.T) { want map[string][]uint8 wantErr string }{ + // valid kubeConfigPath is specified { namespace: "test-namespace", name: "mysecret", @@ -86,10 +87,12 @@ func Test_getObject(t *testing.T) { func Test_getKubeConfig(t *testing.T) { homeDir, _ := os.UserHomeDir() testcases := []struct { - config config.MapConfig - want string - wantErr string + config config.MapConfig + kubeConfigEnvVar string + want string + wantErr string }{ + // kubeConfigPath is set { config: config.MapConfig{ M: map[string]interface{}{ @@ -107,11 +110,42 @@ func Test_getKubeConfig(t *testing.T) { want: "", wantErr: "kubeConfigPath URI parameter is set but path /tmp/does-not-exist does not exist.", }, + // KUBECONFIG specified path is set + { + config: config.MapConfig{ + M: map[string]interface{}{}, + }, + kubeConfigEnvVar: fmt.Sprintf("%s/.kube/config", homeDir), + want: fmt.Sprintf("%s/.kube/config", homeDir), + wantErr: "", + }, + // KUBECONFIG specified path does not exist + { + config: config.MapConfig{ + M: map[string]interface{}{}, + }, + kubeConfigEnvVar: "/tmp/does-not-exist", + want: "", + wantErr: "KUBECONFIG environment variable is set but path /tmp/does-not-exist does not exist.", + }, + // defaultPath exists + { + config: config.MapConfig{ + M: map[string]interface{}{}, + }, + kubeConfigEnvVar: "", + want: fmt.Sprintf("%s/.kube/config", homeDir), + wantErr: "", + }, } for i := range testcases { tc := testcases[i] t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + os.Unsetenv("KUBECONFIG") + if tc.kubeConfigEnvVar != "" { + os.Setenv("KUBECONFIG", tc.kubeConfigEnvVar) + } got, err := getKubeConfig(tc.config) if err != nil { if err.Error() != tc.wantErr { @@ -129,11 +163,13 @@ func Test_getKubeConfig(t *testing.T) { }) } } + func Test_getKubeContext(t *testing.T) { testcases := []struct { config config.MapConfig want string }{ + // Valid kubeContext is specified { config: config.MapConfig{ M: map[string]interface{}{ @@ -169,16 +205,54 @@ func Test_GetString(t *testing.T) { want string wantErr string }{ + // Valid path is specified { path: "v1/Secret/test-namespace/mysecret/key", want: "p4ssw0rd", wantErr: "", }, + // Invalid path is specified + { + path: "v1/Secret/test-namespace/mysecret/key/more/path", + want: "", + wantErr: "Invalid path v1/Secret/test-namespace/mysecret/key/more/path. Path must be in the format ////", + }, + // Bad path is specified + { + path: "bad/data/path", + want: "", + wantErr: "Invalid path bad/data/path. Path must be in the format ////", + }, + // Non-existent namespace is specified + { + path: "v1/Secret/badnamespace/secret/key", + want: "", + wantErr: "Unable to get Secret badnamespace/secret: Unable to get the object from Kubernetes: secrets \"secret\" not found", + }, + // Non-existent secret is specified + { + path: "v1/Secret/test-namespace/badsecret/key", + want: "", + wantErr: "Unable to get Secret test-namespace/badsecret: Unable to get the object from Kubernetes: secrets \"badsecret\" not found", + }, + // Non-existent key is requested { path: "v1/Secret/test-namespace/mysecret/non-existent-key", want: "", wantErr: "Key non-existent-key does not exist in test-namespace/mysecret", }, + // Invalid apiVersion specified + { + path: "v2/Secret/test-namespace/mysecret/non-existent-key", + want: "", + wantErr: "Invalid apiVersion v2. Only apiVersion v1 is supported at this time.", + }, + // Invalid kind specified + { + path: "v1/ConfigMap/test-namespace/mysecret/non-existent-key", + want: "", + wantErr: "Invalid kind ConfigMap. Only kind Secret is supported at this time.", + }, } for _, tc := range tests { t.Run(tc.path, func(t *testing.T) {