diff --git a/.github/PAUL.yaml b/.github/PAUL.yaml index 8983cc0b4..1b298aedb 100644 --- a/.github/PAUL.yaml +++ b/.github/PAUL.yaml @@ -47,6 +47,11 @@ pull_requests: If this is your first time contributing, please make sure to read the [Developer](https://www.external-secrets.io/contributing-devguide/) and [Contributing Process](https://www.external-secrets.io/contributing-process/) guides. Please also mind and follow our [Code of Conduct](https://www.external-secrets.io/contributing-coc/). + + Useful commands: + - `make fmt`: Formats the code + - `make check-diff`: Ensures the branch is clean + - `make reviewable`: Ensures a PR is ready for review # Enables the /cat command cats_enabled: true # enables the /dog command diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 113d37c0a..18b5d1d92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: env: # Common versions GO_VERSION: '1.16' - GOLANGCI_VERSION: 'v1.33' + GOLANGCI_VERSION: 'v1.42.1' # list of available versions: https://storage.googleapis.com/kubebuilder-tools # TODO: 1.21.2 does not shut down properly with controller-runtime 0.9.2 KUBEBUILDER_TOOLS_VERSION: '1.20.2' @@ -61,14 +61,14 @@ jobs: echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache the Go Build Cache - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.build-cache }} key: ${{ runner.os }}-build-lint-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-lint- - name: Cache Go Dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.mod-cache }} key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} @@ -107,21 +107,25 @@ jobs: echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache the Go Build Cache - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.build-cache }} key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-check-diff- - name: Cache Go Dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.mod-cache }} key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-pkg- + # Check DIff also runs Reviewable which needs golangci-lint installed - name: Check Diff - run: make check-diff + run: | + wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.42.1 + export PATH=$PATH:./bin + make check-diff unit-tests: runs-on: ubuntu-18.04 @@ -147,14 +151,14 @@ jobs: echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache the Go Build Cache - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.build-cache }} key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-unit-tests- - name: Cache Go Dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.mod-cache }} key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} @@ -167,7 +171,7 @@ jobs: sudo tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz - name: Cache envtest binaries - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: /usr/local/kubebuilder key: ${{ runner.os }}-kubebuilder-${{env.KUBEBUILDER_TOOLS_VERSION}} @@ -214,14 +218,14 @@ jobs: echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache the Go Build Cache - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.build-cache }} key: ${{ runner.os }}-build-publish-artifacts-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-publish-artifacts- - name: Cache Go Dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.mod-cache }} key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b315893d0..030ebe090 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -50,14 +50,14 @@ jobs: echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache the Go Build Cache - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.build-cache }} key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-unit-tests- - name: Cache Go Dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.mod-cache }} key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} @@ -113,14 +113,14 @@ jobs: echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache the Go Build Cache - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.build-cache }} key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-unit-tests- - name: Cache Go Dependencies - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 with: path: ${{ steps.go.outputs.mod-cache }} key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} @@ -149,7 +149,7 @@ jobs: make test.e2e # Update check run called "integration-fork" - - uses: actions/github-script@v5 + - uses: actions/github-script@v1 id: update-check-run if: ${{ always() }} env: @@ -165,19 +165,19 @@ jobs: pull_number: process.env.number }); const ref = pull.head.sha; - + console.log("\n\nPR sha: " + ref) const { data: checks } = await github.checks.listForRef({ ...context.repo, ref }); - + console.log("\n\nPR CHECKS: " + checks) const check = checks.check_runs.filter(c => c.name === process.env.job); - + console.log("\n\nPR Filtered CHECK: " + check) + console.log(check) const { data: result } = await github.checks.update({ ...context.repo, check_run_id: check[0].id, status: 'completed', conclusion: process.env.conclusion }); - return result; diff --git a/.golangci.yaml b/.golangci.yaml index 65337f327..7aa326bd4 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -32,14 +32,10 @@ linters-settings: min-complexity: 16 goheader: template-path: ./hack/boilerplate.go.txt - golint: - min-confidence: 0 govet: check-shadowing: false lll: line-length: 300 - maligned: - suggest-new: true misspell: locale: US @@ -62,7 +58,6 @@ linters: - gocritic - godot - gofmt - - golint - goprintffuncname - gosec - gosimple @@ -70,13 +65,12 @@ linters: - ineffassign - interfacer - lll - - maligned - misspell - nakedret - nolintlint - prealloc + - revive - rowserrcheck - - scopelint - sqlclosecheck - staticcheck - structcheck @@ -102,7 +96,6 @@ issues: - errcheck - dupl - gosec - - scopelint - unparam - lll diff --git a/Dockerfile b/Dockerfile index 5a1ca119a..723450f19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.14.2 +FROM gcr.io/distroless/static ARG TARGETOS ARG TARGETARCH COPY bin/external-secrets-${TARGETOS}-${TARGETARCH} /bin/external-secrets diff --git a/Makefile b/Makefile index ff918b1eb..3c03579cd 100644 --- a/Makefile +++ b/Makefile @@ -65,12 +65,10 @@ FAIL = (echo ${TIME} ${RED}[FAIL]${CNone} && false) # ==================================================================================== # Conformance -# Ensure a PR is ready for review. -reviewable: generate helm.generate +reviewable: generate helm.generate lint ## Ensure a PR is ready for review. @go mod tidy -# Ensure branch is clean. -check-diff: reviewable +check-diff: reviewable ## Ensure branch is clean. @$(INFO) checking that branch is clean @test -z "$$(git status --porcelain)" || (echo "$$(git status --porcelain)" && $(FAIL)) @$(OK) branch is clean @@ -91,7 +89,7 @@ test.e2e: generate ## Run e2e tests @$(OK) go test unit-tests .PHONY: build -build: $(addprefix build-,$(ARCH)) +build: $(addprefix build-,$(ARCH)) ## Build binary .PHONY: build-% build-%: generate ## Build binary for the specified arch @@ -100,27 +98,26 @@ build-%: generate ## Build binary for the specified arch go build -o '$(OUTPUT_DIR)/external-secrets-linux-$*' main.go @$(OK) go build $* -# Check install of golanci-lint -lint.check: +lint.check: ## Check install of golanci-lint @if ! golangci-lint --version > /dev/null 2>&1; then \ echo -e "\033[0;33mgolangci-lint is not installed: run \`\033[0;32mmake lint.install\033[0m\033[0;33m\` or install it from https://golangci-lint.run\033[0m"; \ exit 1; \ fi -# installs golangci-lint to the go bin dir -lint.install: +lint.install: ## Install golangci-lint to the go bin dir @if ! golangci-lint --version > /dev/null 2>&1; then \ echo "Installing golangci-lint"; \ - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOBIN) v1.33.0; \ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.42.1; \ fi -lint: lint.check ## run golangci-lint +lint: lint.check ## Run golangci-lint @if ! golangci-lint run; then \ echo -e "\033[0;33mgolangci-lint failed: some checks can be fixed with \`\033[0;32mmake fmt\033[0m\033[0;33m\`\033[0m"; \ exit 1; \ fi + @$(OK) Finished linting -fmt: lint.check ## ensure consistent code style +fmt: lint.check ## Ensure consistent code style @go mod tidy @go fmt ./... @golangci-lint run --fix > /dev/null 2>&1 || true @@ -142,20 +139,17 @@ generate: ## Generate code and crds # This is for running out-of-cluster locally, and is for convenience. # For more control, try running the binary directly with different arguments. -run: generate +run: generate ## Run app locally (without a k8s cluster) go run ./main.go -# Generate manifests from helm chart -manifests: helm.generate +manifests: helm.generate ## Generate manifests from helm chart mkdir -p $(OUTPUT_DIR)/deploy/manifests helm template external-secrets $(HELM_DIR) -f deploy/manifests/helm-values.yaml > $(OUTPUT_DIR)/deploy/manifests/external-secrets.yaml -# Install CRDs into a cluster. This is for convenience. -crds.install: generate +crds.install: generate ## Install CRDs into a cluster. This is for convenience kubectl apply -f $(CRD_DIR) -# Uninstall CRDs from a cluster. This is for convenience. -crds.uninstall: +crds.uninstall: ## Uninstall CRDs from a cluster. This is for convenience kubectl delete -f $(CRD_DIR) # ==================================================================================== @@ -173,8 +167,7 @@ helm.build: helm.generate ## Build helm chart @mv $(OUTPUT_DIR)/chart/external-secrets-$(HELM_VERSION).tgz $(OUTPUT_DIR)/chart/external-secrets.tgz @$(OK) helm package -# Copy crds to helm chart directory -helm.generate: helm.docs +helm.generate: helm.docs ## Copy crds to helm chart directory @cp $(CRD_DIR)/*.yaml $(HELM_DIR)/templates/crds/ # Add helm if statement for controlling the install of CRDs @for i in $(HELM_DIR)/templates/crds/*.yaml; do \ @@ -189,24 +182,24 @@ helm.generate: helm.docs # ==================================================================================== # Documentation .PHONY: docs -docs: generate +docs: generate ## Generate docs $(MAKE) -C ./hack/api-docs build .PHONY: serve-docs -serve-docs: +serve-docs: ## Serve docs $(MAKE) -C ./hack/api-docs serve # ==================================================================================== # Build Artifacts -build.all: docker.build helm.build +build.all: docker.build helm.build ## Build all artifacts (docker image, helm chart) docker.build: $(addprefix build-,$(ARCH)) ## Build the docker image @$(INFO) docker build @docker build . $(BUILD_ARGS) -t $(IMAGE_REGISTRY):$(VERSION) @$(OK) docker build -docker.push: +docker.push: ## Push the docker image to the registry @$(INFO) docker push @docker push $(IMAGE_REGISTRY):$(VERSION) @$(OK) docker push @@ -216,7 +209,7 @@ docker.push: RELEASE_TAG ?= main SOURCE_TAG ?= $(VERSION) -docker.promote: +docker.promote: ## Promote the docker image to the registry @$(INFO) promoting $(SOURCE_TAG) to $(RELEASE_TAG) docker manifest inspect $(IMAGE_REGISTRY):$(SOURCE_TAG) > .tagmanifest for digest in $$(jq -r '.manifests[].digest' < .tagmanifest); do \ @@ -231,5 +224,5 @@ docker.promote: # Help # only comments after make target name are shown as help text -help: ## displays this help message +help: ## Displays this help message @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s : | sort)" diff --git a/README.md b/README.md index 5f75af8c4..11dfb4e7b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Multiple people and organizations are joining efforts to create a single Externa - [AWS Secrets Manager](https://external-secrets.io/provider-aws-secrets-manager/) - [AWS Parameter Store](https://external-secrets.io/provider-aws-parameter-store/) +- [Akeyless](https://www.akeyless.io/) - [Hashicorp Vault](https://www.vaultproject.io/) - [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/) - [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/) @@ -20,7 +21,7 @@ Multiple people and organizations are joining efforts to create a single Externa - [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) - [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) - [Alibaba Cloud KMS](https://www.alibabacloud.com/product/kms) (Docs still missing, PRs welcomed!) -- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) +- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) ## Stability and Support Level @@ -28,10 +29,10 @@ Multiple people and organizations are joining efforts to create a single Externa | Provider | Stability | Contact | | ------------------------------------------------------------------------ | :-------: | ---------------------------------------------: | -| [AWS SM](https://external-secrets.io/provider-aws-secrets-manager/) | alpha | [ESO Org](https://github.com/external-secrets) | -| [AWS PS](https://external-secrets.io/provider-aws-parameter-store/) | alpha | [ESO Org](https://github.com/external-secrets) | -| [Hashicorp Vault](https://external-secrets.io/provider-hashicorp-vault/) | alpha | [ESO Org](https://github.com/external-secrets) | -| [GCP SM](https://external-secrets.io/provider-google-secrets-manager/) | alpha | [ESO Org](https://github.com/external-secrets) | +| [AWS SM](https://external-secrets.io/provider-aws-secrets-manager/) | beta | [ESO Org](https://github.com/external-secrets) | +| [AWS PS](https://external-secrets.io/provider-aws-parameter-store/) | beta | [ESO Org](https://github.com/external-secrets) | +| [Hashicorp Vault](https://external-secrets.io/provider-hashicorp-vault/) | stable | [ESO Org](https://github.com/external-secrets) | +| [GCP SM](https://external-secrets.io/provider-google-secrets-manager/) | beta | [ESO Org](https://github.com/external-secrets) | ### Community maintained: @@ -43,6 +44,7 @@ Multiple people and organizations are joining efforts to create a single Externa | [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) | alpha | [@Jabray5](https://github.com/Jabray5) | | Alibaba Cloud KMS | alpha | [@ElsaChelala](https://github.com/ElsaChelala) | | [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) | alpha | [@KianTigger](https://github.com/KianTigger) | +| [Akeyless]( https://external-secrets.io/provider-akeyless) | alpha | [@renanaAkeyless](https://github.com/renanaAkeyless) | ## Documentation diff --git a/apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go b/apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go new file mode 100644 index 000000000..14ab4e708 --- /dev/null +++ b/apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go @@ -0,0 +1,42 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" +) + +// AkeylessProvider Configures an store to sync secrets using Akeyless KV. +type AkeylessProvider struct { + + // Akeyless GW API Url from which the secrets to be fetched from. + AkeylessGWApiURL *string `json:"akeylessGWApiURL"` + + // Auth configures how the operator authenticates with Akeyless. + Auth *AkeylessAuth `json:"authSecretRef"` +} + +type AkeylessAuth struct { + SecretRef AkeylessAuthSecretRef `json:"secretRef"` +} + +// AkeylessAuthSecretRef +//AKEYLESS_ACCESS_TYPE_PARAM: AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME. +type AkeylessAuthSecretRef struct { + // The SecretAccessID is used for authentication + AccessID esmeta.SecretKeySelector `json:"accessID,omitempty"` + AccessType esmeta.SecretKeySelector `json:"accessType,omitempty"` + AccessTypeParam esmeta.SecretKeySelector `json:"accessTypeParam,omitempty"` +} diff --git a/apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go b/apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go index cd80a3cf1..71389df14 100644 --- a/apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go +++ b/apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go @@ -16,14 +16,41 @@ package v1alpha1 import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1" +// AuthType describes how to authenticate to the Azure Keyvault +// Only one of the following auth types may be specified. +// If none of the following auth type is specified, the default one +// is ServicePrincipal. +// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity +type AuthType string + +const ( + // Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret. + ServicePrincipal AuthType = "ServicePrincipal" + + // Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister. + ManagedIdentity AuthType = "ManagedIdentity" +) + // Configures an store to sync secrets using Azure KV. type AzureKVProvider struct { + // Auth type defines how to authenticate to the keyvault service. + // Valid values are: + // - "ServicePrincipal" (default): Using a service principal (tenantId, clientId, clientSecret) + // - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity) + // +optional + // +kubebuilder:default=ServicePrincipal + AuthType *AuthType `json:"authType,omitempty"` // Vault Url from which the secrets to be fetched from. VaultURL *string `json:"vaultUrl"` - // TenantID configures the Azure Tenant to send requests to. - TenantID *string `json:"tenantId"` - // Auth configures how the operator authenticates with Azure. - AuthSecretRef *AzureKVAuth `json:"authSecretRef"` + // TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type. + // +optional + TenantID *string `json:"tenantId,omitempty"` + // Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type. + // +optional + AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"` + // If multiple Managed Identity is assigned to the pod, you can select the one to be used + // +optional + IdentityID *string `json:"identityId,omitempty"` } // Configuration used to authenticate with Azure. diff --git a/apis/externalsecrets/v1alpha1/secretstore_types.go b/apis/externalsecrets/v1alpha1/secretstore_types.go index b570193fa..983b6bc69 100644 --- a/apis/externalsecrets/v1alpha1/secretstore_types.go +++ b/apis/externalsecrets/v1alpha1/secretstore_types.go @@ -46,6 +46,10 @@ type SecretStoreProvider struct { // +optional AzureKV *AzureKVProvider `json:"azurekv,omitempty"` + // Akeyless configures this store to sync secrets using Akeyless Vault provider + // +optional + Akeyless *AkeylessProvider `json:"akeyless,omitempty"` + // Vault configures this store to sync secrets using Hashi provider // +optional Vault *VaultProvider `json:"vault,omitempty"` diff --git a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go index 30a6dce42..9057b9141 100644 --- a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go @@ -102,6 +102,65 @@ func (in *AWSProvider) DeepCopy() *AWSProvider { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AkeylessAuth) DeepCopyInto(out *AkeylessAuth) { + *out = *in + in.SecretRef.DeepCopyInto(&out.SecretRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuth. +func (in *AkeylessAuth) DeepCopy() *AkeylessAuth { + if in == nil { + return nil + } + out := new(AkeylessAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AkeylessAuthSecretRef) DeepCopyInto(out *AkeylessAuthSecretRef) { + *out = *in + in.AccessID.DeepCopyInto(&out.AccessID) + in.AccessType.DeepCopyInto(&out.AccessType) + in.AccessTypeParam.DeepCopyInto(&out.AccessTypeParam) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuthSecretRef. +func (in *AkeylessAuthSecretRef) DeepCopy() *AkeylessAuthSecretRef { + if in == nil { + return nil + } + out := new(AkeylessAuthSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AkeylessProvider) DeepCopyInto(out *AkeylessProvider) { + *out = *in + if in.AkeylessGWApiURL != nil { + in, out := &in.AkeylessGWApiURL, &out.AkeylessGWApiURL + *out = new(string) + **out = **in + } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(AkeylessAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessProvider. +func (in *AkeylessProvider) DeepCopy() *AkeylessProvider { + if in == nil { + return nil + } + out := new(AkeylessProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AlibabaAuth) DeepCopyInto(out *AlibabaAuth) { *out = *in @@ -183,6 +242,11 @@ func (in *AzureKVAuth) DeepCopy() *AzureKVAuth { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) { *out = *in + if in.AuthType != nil { + in, out := &in.AuthType, &out.AuthType + *out = new(AuthType) + **out = **in + } if in.VaultURL != nil { in, out := &in.VaultURL, &out.VaultURL *out = new(string) @@ -198,6 +262,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) { *out = new(AzureKVAuth) (*in).DeepCopyInto(*out) } + if in.IdentityID != nil { + in, out := &in.IdentityID, &out.IdentityID + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKVProvider. @@ -794,6 +863,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) { *out = new(AzureKVProvider) (*in).DeepCopyInto(*out) } + if in.Akeyless != nil { + in, out := &in.Akeyless, &out.Akeyless + *out = new(AkeylessProvider) + (*in).DeepCopyInto(*out) + } if in.Vault != nil { in, out := &in.Vault, &out.Vault *out = new(VaultProvider) diff --git a/deploy/charts/external-secrets/Chart.yaml b/deploy/charts/external-secrets/Chart.yaml index 031bc4877..add69168d 100644 --- a/deploy/charts/external-secrets/Chart.yaml +++ b/deploy/charts/external-secrets/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: external-secrets description: External secret management for Kubernetes type: application -version: "0.3.6" -appVersion: "v0.3.6" +version: "0.3.9" +appVersion: "v0.3.9" kubeVersion: ">= 1.11.0-0" keywords: - kubernetes-external-secrets diff --git a/deploy/charts/external-secrets/README.md b/deploy/charts/external-secrets/README.md index ab2b4e5a8..a2caf39af 100644 --- a/deploy/charts/external-secrets/README.md +++ b/deploy/charts/external-secrets/README.md @@ -4,7 +4,7 @@ [//]: # (README.md generated by gotmpl. DO NOT EDIT.) -![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.6](https://img.shields.io/badge/Version-0.3.6-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.9](https://img.shields.io/badge/Version-0.3.9-informational?style=flat-square) External secret management for Kubernetes diff --git a/deploy/crds/external-secrets.io_clustersecretstores.yaml b/deploy/crds/external-secrets.io_clustersecretstores.yaml index eb8458c2e..c226225a0 100644 --- a/deploy/crds/external-secrets.io_clustersecretstores.yaml +++ b/deploy/crds/external-secrets.io_clustersecretstores.yaml @@ -54,6 +54,94 @@ spec: maxProperties: 1 minProperties: 1 properties: + akeyless: + description: Akeyless configures this store to sync secrets using + Akeyless Vault provider + properties: + akeylessGWApiURL: + description: Akeyless GW API Url from which the secrets to + be fetched from. + type: string + authSecretRef: + description: Auth configures how the operator authenticates + with Akeyless. + properties: + secretRef: + description: 'AkeylessAuthSecretRef AKEYLESS_ACCESS_TYPE_PARAM: + AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.' + properties: + accessID: + description: The SecretAccessID is used for authentication + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessType: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessTypeParam: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + type: object + required: + - secretRef + type: object + required: + - akeylessGWApiURL + - authSecretRef + type: object alibaba: description: Alibaba configures this store to sync secrets using Alibaba Cloud provider @@ -222,7 +310,7 @@ spec: properties: authSecretRef: description: Auth configures how the operator authenticates - with Azure. + with Azure. Required for ServicePrincipal auth type. properties: clientId: description: The Azure clientId of the service principle @@ -266,17 +354,30 @@ spec: - clientId - clientSecret type: object + authType: + default: ServicePrincipal + description: 'Auth type defines how to authenticate to the + keyvault service. Valid values are: - "ServicePrincipal" + (default): Using a service principal (tenantId, clientId, + clientSecret) - "ManagedIdentity": Using Managed Identity + assigned to the pod (see aad-pod-identity)' + enum: + - ServicePrincipal + - ManagedIdentity + type: string + identityId: + description: If multiple Managed Identity is assigned to the + pod, you can select the one to be used + type: string tenantId: description: TenantID configures the Azure Tenant to send - requests to. + requests to. Required for ServicePrincipal auth type. type: string vaultUrl: description: Vault Url from which the secrets to be fetched from. type: string required: - - authSecretRef - - tenantId - vaultUrl type: object gcpsm: diff --git a/deploy/crds/external-secrets.io_secretstores.yaml b/deploy/crds/external-secrets.io_secretstores.yaml index 4c7b3d8fc..79a5b92ec 100644 --- a/deploy/crds/external-secrets.io_secretstores.yaml +++ b/deploy/crds/external-secrets.io_secretstores.yaml @@ -54,6 +54,94 @@ spec: maxProperties: 1 minProperties: 1 properties: + akeyless: + description: Akeyless configures this store to sync secrets using + Akeyless Vault provider + properties: + akeylessGWApiURL: + description: Akeyless GW API Url from which the secrets to + be fetched from. + type: string + authSecretRef: + description: Auth configures how the operator authenticates + with Akeyless. + properties: + secretRef: + description: 'AkeylessAuthSecretRef AKEYLESS_ACCESS_TYPE_PARAM: + AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.' + properties: + accessID: + description: The SecretAccessID is used for authentication + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessType: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessTypeParam: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + type: object + required: + - secretRef + type: object + required: + - akeylessGWApiURL + - authSecretRef + type: object alibaba: description: Alibaba configures this store to sync secrets using Alibaba Cloud provider @@ -222,7 +310,7 @@ spec: properties: authSecretRef: description: Auth configures how the operator authenticates - with Azure. + with Azure. Required for ServicePrincipal auth type. properties: clientId: description: The Azure clientId of the service principle @@ -266,17 +354,30 @@ spec: - clientId - clientSecret type: object + authType: + default: ServicePrincipal + description: 'Auth type defines how to authenticate to the + keyvault service. Valid values are: - "ServicePrincipal" + (default): Using a service principal (tenantId, clientId, + clientSecret) - "ManagedIdentity": Using Managed Identity + assigned to the pod (see aad-pod-identity)' + enum: + - ServicePrincipal + - ManagedIdentity + type: string + identityId: + description: If multiple Managed Identity is assigned to the + pod, you can select the one to be used + type: string tenantId: description: TenantID configures the Azure Tenant to send - requests to. + requests to. Required for ServicePrincipal auth type. type: string vaultUrl: description: Vault Url from which the secrets to be fetched from. type: string required: - - authSecretRef - - tenantId - vaultUrl type: object gcpsm: diff --git a/docs/provider-akeyless.md b/docs/provider-akeyless.md new file mode 100644 index 000000000..1843868dd --- /dev/null +++ b/docs/provider-akeyless.md @@ -0,0 +1,68 @@ +## Akeyless Vault + +External Secrets Operator integrates with [Akeyless API](https://docs.akeyless.io/reference#v2). + +### Authentication + +The API requires an access-id, access-type and access-Type-param. + +The supported auth-methods and their params are: + +| accessType | accessTypeParam | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `api_key` | The access key. | +| `k8s` | The k8s configuration name | +| `aws_iam` | - | +| `gcp` | The gcp audience | +| `azure_ad` | azure object id (optional) | + +form more information about [Akeyless Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods) + +### Akeless credentials secret + +Create a secret containing your credentials: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: akeylss-secret-creds +type: Opaque +stringData: + accessId: "p-XXXX" + accessType: # k8s/aws_iam/gcp/azure_ad/api_key + accessTypeParam: # can be one of the following: k8s-conf-name/gcp-audience/azure-obj-id/access-key +``` + +### Update secret store +Be sure the `akeyless` provider is listed in the `Kind=SecretStore` and the `akeylessGWApiURL` is set (def: "https://api.akeless.io". + +```yaml +{% include 'akeyless-secret-store.yaml' %} +``` + +### Creating external secret + +To get a secret from Akeyless and secret it on the Kubernetes cluster, a `Kind=ExternalSecret` is needed. + +```yaml +{% include 'akeyless-external-secret.yaml' %} +``` + +#### Using DataFrom + +DataFrom can be used to get a secret as a JSON string and attempt to parse it. + +```yaml +{% include 'akeyless-external-secret-json.yaml' %} +``` + +### Getting the Kubernetes secret +The operator will fetch the secret and inject it as a `Kind=Secret`. +``` +kubectl get secret akeyless-secret-to-create -o jsonpath='{.data.secretKey}' | base64 -d +``` + +``` +kubectl get secret akeyless-secret-to-create-json -o jsonpath='{.data}' +``` diff --git a/docs/provider-azure-key-vault.md b/docs/provider-azure-key-vault.md index 7e44dd8a5..62b7a4f9a 100644 --- a/docs/provider-azure-key-vault.md +++ b/docs/provider-azure-key-vault.md @@ -7,23 +7,37 @@ External Secrets Operator integrates with [Azure Key vault](https://azure.micros ### Authentication -At the moment, we only support [service principals](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication) authentication. +We support Service Principals and Managed Identity [authentication](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication). + +To use Managed Identity authentication, you should use [aad-pod-identity](https://azure.github.io/aad-pod-identity/docs/) to assign the identity to external-secrets operator. To add the selector to external-secrets operator, use `podLabels` in your values.yaml in case of Helm installation of external-secrets. #### Service Principal key authentication A service Principal client and Secret is created and the JSON keyfile is stored in a `Kind=Secret`. The `ClientID` and `ClientSecret` should be configured for the secret. This service principal should have proper access rights to the keyvault to be managed by the operator +#### Managed Identity authentication + +A Managed Identity should be created in Azure, and that Identity should have proper rights to the keyvault to be managed by the operator. + +If there are multiple Managed Identitites for different keyvaults, the operator should have been assigned all identities via [aad-pod-identity](https://azure.github.io/aad-pod-identity/docs/), then the SecretStore configuration should include the Id of the idenetity to be used via the `identityId` field. + ```yaml {% include 'azkv-credentials-secret.yaml' %} ``` ### Update secret store -Be sure the `azkv` provider is listed in the `Kind=SecretStore` +Be sure the `azurekv` provider is listed in the `Kind=SecretStore` ```yaml {% include 'azkv-secret-store.yaml' %} ``` +Or in case of Managed Idenetity authentication: + +```yaml +{% include 'azkv-secret-store-mi.yaml' %} +``` + ### Object Types Azure KeyVault manages different [object types](https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-types), we support `keys`, `secrets` and `certificates`. Simply prefix the key with `key`, `secret` or `cert` to retrieve the desired type (defaults to secret). diff --git a/docs/snippets/akeyless-credentials-secret.yaml b/docs/snippets/akeyless-credentials-secret.yaml new file mode 100644 index 000000000..d31cf8243 --- /dev/null +++ b/docs/snippets/akeyless-credentials-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: akeylss-secret-creds +type: Opaque +stringData: + accessId: "p-XXXX" + accessType: # k8s/aws_iam/gcp/azure_ad/api_key + accessTypeParam: # can be one of the following: k8s-conf-name/gcp-audience/azure-obj-id/access-key + + + diff --git a/docs/snippets/akeyless-external-secret-json.yaml b/docs/snippets/akeyless-external-secret-json.yaml new file mode 100644 index 000000000..20744db62 --- /dev/null +++ b/docs/snippets/akeyless-external-secret-json.yaml @@ -0,0 +1,18 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: ExternalSecret +metadata: + name: akeyless-external-secret-example-json +spec: + refreshInterval: 1h + + secretStoreRef: + kind: SecretStore + name: akeyless-secret-store # Must match SecretStore on the cluster + + target: + name: akeyless-secret-to-create-json # Name for the secret to be created on the cluster + creationPolicy: Owner + + # for json formatted secrets: each key in the json will be used as the secret key in the SECRET k8s target object + dataFrom: + - key: secret-name # Full path of the secret on Akeyless diff --git a/docs/snippets/akeyless-external-secret.yaml b/docs/snippets/akeyless-external-secret.yaml new file mode 100644 index 000000000..27eed4ba5 --- /dev/null +++ b/docs/snippets/akeyless-external-secret.yaml @@ -0,0 +1,19 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: ExternalSecret +metadata: + name: akeyless-external-secret-example +spec: + refreshInterval: 1h + + secretStoreRef: + kind: SecretStore + name: akeyless-secret-store # Must match SecretStore on the cluster + + target: + name: akeyless-secret-to-create # Name for the secret to be created on the cluster + creationPolicy: Owner + + data: + - secretKey: secretKey # Key given to the secret to be created on the cluster + remoteRef: + key: secret-name # Full path of the secret on Akeyless diff --git a/docs/snippets/akeyless-secret-store.yaml b/docs/snippets/akeyless-secret-store.yaml new file mode 100644 index 000000000..b41b2643c --- /dev/null +++ b/docs/snippets/akeyless-secret-store.yaml @@ -0,0 +1,20 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: SecretStore +metadata: + name: akeyless-secret-store +spec: + provider: + akeyless: + # URL of your akeyless API + akeylessGWApiURL: "https://api.akeyless.io" + authSecretRef: + secretRef: + accessID: + name: akeylss-secret-creds + key: accessId + accessType: + name: akeylss-secret-creds + key: accessType + accessTypeParam: + name: akeylss-secret-creds + key: accessTypeParam diff --git a/docs/snippets/azkv-secret-store-mi.yaml b/docs/snippets/azkv-secret-store-mi.yaml new file mode 100644 index 000000000..eee62e711 --- /dev/null +++ b/docs/snippets/azkv-secret-store-mi.yaml @@ -0,0 +1,13 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: SecretStore +metadata: + name: example-secret-store +spec: + provider: + # provider type: azure keyvault + azurekv: + authType: ManagedIdentity + # Optionally set the Id of the Managed Identity, if multiple identities is assignet to external-secrets operator + identityId: "" + # URL of your vault instance, see: https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates + vaultUrl: "https://my-keyvault-name.vault.azure.net" diff --git a/docs/snippets/template-from-secret.yaml b/docs/snippets/template-from-secret.yaml index 56fd1dbbc..2410df631 100644 --- a/docs/snippets/template-from-secret.yaml +++ b/docs/snippets/template-from-secret.yaml @@ -1,5 +1,5 @@ {% raw %} -# define your tempalte in a config map +# define your template in a config map apiVersion: v1 kind: ConfigMap metadata: diff --git a/e2e/Dockerfile b/e2e/Dockerfile index e6fea409d..642a6b799 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -1,8 +1,8 @@ ARG GO_VERSION=1.16 FROM golang:$GO_VERSION-buster as builder -ENV KUBECTL_VERSION="v1.19.2" -ENV HELM_VERSION="v3.3.4" +ENV KUBECTL_VERSION="v1.21.2" +ENV HELM_VERSION="v3.7.1" RUN go get -u github.com/onsi/ginkgo/ginkgo RUN wget -q https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl -O /usr/local/bin/kubectl && \ @@ -10,7 +10,7 @@ RUN wget -q https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_ wget -q https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz -O - | tar -xzO linux-amd64/helm > /usr/local/bin/helm && \ chmod +x /usr/local/bin/helm -FROM alpine:3.12 +FROM alpine:3.15.0 RUN apk add -U --no-cache \ ca-certificates \ bash \ diff --git a/e2e/Makefile b/e2e/Makefile index bd8e54a5e..4d574fabf 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -4,7 +4,7 @@ SHELL := /bin/bash IMG_TAG = test IMG = local/external-secrets-e2e:$(IMG_TAG) -KIND_IMG = "kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9" +KIND_IMG = "kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6" BUILD_ARGS ?= export FOCUS := $(FOCUS) @@ -19,13 +19,14 @@ test: e2e-image ## Run e2e tests against current kube context $(MAKE) -C ../ docker.build \ IMAGE_REGISTRY=local/external-secrets \ VERSION=$(IMG_TAG) \ - ARCH=amd64 + ARCH=amd64 \ + BUILD_ARGS="${BUILD_ARGS} --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux" kind load docker-image --name="external-secrets" local/external-secrets:$(IMG_TAG) kind load docker-image --name="external-secrets" $(IMG) ./run.sh e2e-bin: - CGO_ENABLED=0 ginkgo build . + CGO_ENABLED=0 go run github.com/onsi/ginkgo/ginkgo build . e2e-image: e2e-bin -rm -rf ./k8s/deploy diff --git a/e2e/entrypoint.sh b/e2e/entrypoint.sh index 986e0953a..e1591d967 100755 --- a/e2e/entrypoint.sh +++ b/e2e/entrypoint.sh @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -e +set -euo pipefail NC='\e[0m' BGREEN='\e[32m' @@ -46,8 +46,8 @@ ginkgo_args=( kubectl apply -f /k8s/deploy/crds echo -e "${BGREEN}Running e2e test suite (FOCUS=${FOCUS})...${NC}" -ginkgo "${ginkgo_args[@]}" \ - -focus="${FOCUS}" \ - -skip="\[Serial\]|\[MemoryLeak\]" \ - -nodes="${E2E_NODES}" \ +ACK_GINKGO_RC=true ginkgo "${ginkgo_args[@]}" \ + -focus="${FOCUS}" \ + -skip="\[Serial\]|\[MemoryLeak\]" \ + -nodes="${E2E_NODES}" \ /e2e.test diff --git a/e2e/framework/addon/vault.go b/e2e/framework/addon/vault.go index 5a3f20e21..57bd420ef 100644 --- a/e2e/framework/addon/vault.go +++ b/e2e/framework/addon/vault.go @@ -28,7 +28,7 @@ import ( "os" "time" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v4" vault "github.com/hashicorp/vault/api" // nolint @@ -286,6 +286,7 @@ func (l *Vault) Setup(cfg *Config) error { return l.chart.Setup(cfg) } +// nolint:gocritic func genVaultCertificates(namespace string) ([]byte, []byte, []byte, []byte, []byte, []byte, error) { // gen server ca + certs serverRootCert, serverRootPem, serverRootKey, err := genCARoot() diff --git a/e2e/run.sh b/e2e/run.sh index 429cc3e05..55edfcd7e 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -13,9 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -set -o errexit -set -o nounset -set -o pipefail +set -euo pipefail if ! command -v kind --version &> /dev/null; then echo "kind is not installed. Use the package manager or visit the official site https://kind.sigs.k8s.io/" @@ -56,6 +54,9 @@ kubectl run --rm \ --env="GCP_PROJECT_ID=${GCP_PROJECT_ID:-}" \ --env="AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}" \ --env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \ + --env="AKEYLESS_ACCESS_ID=${AKEYLESS_ACCESS_ID:-}" \ + --env="AKEYLESS_ACCESS_TYPE=${AKEYLESS_ACCESS_TYPE:-}" \ + --env="AKEYLESS_ACCESS_TYPE_PARAM=${AKEYLESS_ACCESS_TYPE_PARAM:-}" \ --env="TENANT_ID=${TENANT_ID:-}" \ --env="VAULT_URL=${VAULT_URL:-}" \ --env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \ diff --git a/e2e/suite/akeyless/akeyless.go b/e2e/suite/akeyless/akeyless.go new file mode 100644 index 000000000..de8ad93b6 --- /dev/null +++ b/e2e/suite/akeyless/akeyless.go @@ -0,0 +1,49 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package akeyless + +import ( + "os" + + // nolint + . "github.com/onsi/ginkgo" + // nolint + . "github.com/onsi/ginkgo/extensions/table" + + "github.com/external-secrets/external-secrets/e2e/framework" + "github.com/external-secrets/external-secrets/e2e/suite/common" +) + +var _ = Describe("[akeyless] ", func() { + f := framework.New("eso-akeyless") + accessID := os.Getenv("AKEYLESS_ACCESS_ID") + accessType := os.Getenv("AKEYLESS_ACCESS_TYPE") + accessTypeParam := os.Getenv("AKEYLESS_ACCESS_TYPE_PARAM") + prov := newAkeylessProvider(f, accessID, accessType, accessTypeParam) + + DescribeTable("sync secrets", framework.TableFunc(f, prov), + Entry(common.SimpleDataSync(f)), + Entry(common.NestedJSONWithGJSON(f)), + Entry(common.JSONDataFromSync(f)), + Entry(common.JSONDataWithProperty(f)), + Entry(common.JSONDataWithTemplate(f)), + Entry(common.DockerJSONConfig(f)), + Entry(common.DataPropertyDockerconfigJSON(f)), + Entry(common.SSHKeySync(f)), + Entry(common.SSHKeySyncDataProperty(f)), + Entry(common.SyncWithoutTargetName(f)), + Entry(common.JSONDataWithoutTargetName(f)), + ) +}) diff --git a/e2e/suite/akeyless/provider.go b/e2e/suite/akeyless/provider.go new file mode 100644 index 000000000..8528c41db --- /dev/null +++ b/e2e/suite/akeyless/provider.go @@ -0,0 +1,227 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package akeyless + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure" + gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp" + "github.com/akeylesslabs/akeyless-go/v2" + + //nolint + . "github.com/onsi/ginkgo" + + //nolint + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" + esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" + "github.com/external-secrets/external-secrets/e2e/framework" +) + +type akeylessProvider struct { + accessID string + accessType string + accessTypeParam string + framework *framework.Framework + restAPIClient *akeyless.V2ApiService +} + +var apiErr akeyless.GenericOpenAPIError + +const DefServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + +func newAkeylessProvider(f *framework.Framework, accessID, accessType, accessTypeParam string) *akeylessProvider { + prov := &akeylessProvider{ + accessID: accessID, + accessType: accessType, + accessTypeParam: accessTypeParam, + framework: f, + } + + restAPIClient := akeyless.NewAPIClient(&akeyless.Configuration{ + Servers: []akeyless.ServerConfiguration{ + { + URL: "https://api.akeyless.io", + }, + }, + }).V2Api + + prov.restAPIClient = restAPIClient + + BeforeEach(prov.BeforeEach) + return prov +} + +// CreateSecret creates a secret. +func (a *akeylessProvider) CreateSecret(key, val string) { + token, err := a.GetToken() + Expect(err).ToNot(HaveOccurred()) + + ctx := context.Background() + gsvBody := akeyless.CreateSecret{ + Name: key, + Value: val, + Token: &token, + } + + _, _, err = a.restAPIClient.CreateSecret(ctx).Body(gsvBody).Execute() + Expect(err).ToNot(HaveOccurred()) +} + +func (a *akeylessProvider) DeleteSecret(key string) { + token, err := a.GetToken() + Expect(err).ToNot(HaveOccurred()) + + ctx := context.Background() + gsvBody := akeyless.DeleteItem{ + Name: key, + Token: &token, + } + + _, _, err = a.restAPIClient.DeleteItem(ctx).Body(gsvBody).Execute() + Expect(err).ToNot(HaveOccurred()) +} + +func (a *akeylessProvider) BeforeEach() { + // Creating an Akeyless secret + akeylessCreds := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "provider-secret", + Namespace: a.framework.Namespace.Name, + }, + StringData: map[string]string{ + "access-id": a.accessID, + "access-type": a.accessType, + "access-type-param": a.accessTypeParam, + }, + } + err := a.framework.CRClient.Create(context.Background(), akeylessCreds) + Expect(err).ToNot(HaveOccurred()) + + // Creating Akeyless secret store + secretStore := &esv1alpha1.SecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: a.framework.Namespace.Name, + Namespace: a.framework.Namespace.Name, + }, + Spec: esv1alpha1.SecretStoreSpec{ + Provider: &esv1alpha1.SecretStoreProvider{ + Akeyless: &esv1alpha1.AkeylessProvider{ + Auth: &esv1alpha1.AkeylessAuth{ + SecretRef: esv1alpha1.AkeylessAuthSecretRef{ + AccessID: esmeta.SecretKeySelector{ + Name: "access-id-secret", + Key: "access-id", + }, + AccessType: esmeta.SecretKeySelector{ + Name: "access-type-secret", + Key: "access-type", + }, + AccessTypeParam: esmeta.SecretKeySelector{ + Name: "access-type-param-secert", + Key: "access-type-param", + }, + }, + }, + }, + }, + }, + } + err = a.framework.CRClient.Create(context.Background(), secretStore) + Expect(err).ToNot(HaveOccurred()) +} + +func (a *akeylessProvider) GetToken() (string, error) { + ctx := context.Background() + authBody := akeyless.NewAuthWithDefaults() + authBody.AccessId = akeyless.PtrString(a.accessID) + + if a.accessType == "api_key" { + authBody.AccessKey = akeyless.PtrString(a.accessTypeParam) + } else if a.accessType == "k8s" { + jwtString, err := readK8SServiceAccountJWT() + if err != nil { + return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err) + } + K8SAuthConfigName := a.accessTypeParam + authBody.AccessType = akeyless.PtrString(a.accessType) + authBody.K8sServiceAccountToken = akeyless.PtrString(jwtString) + authBody.K8sAuthConfigName = akeyless.PtrString(K8SAuthConfigName) + } else { + cloudID, err := a.getCloudID(a.accessType, a.accessTypeParam) + if err != nil { + return "", fmt.Errorf("Require Cloud ID " + err.Error()) + } + authBody.AccessType = akeyless.PtrString(a.accessType) + authBody.CloudId = akeyless.PtrString(cloudID) + } + + authOut, _, err := a.restAPIClient.Auth(ctx).Body(*authBody).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("authentication failed: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("authentication failed: %w", err) + } + + token := authOut.GetToken() + return token, nil +} + +func (a *akeylessProvider) getCloudID(provider, accTypeParam string) (string, error) { + var cloudID string + var err error + + switch provider { + case "azure_ad": + cloudID, err = azure_cloud_id.GetCloudId(accTypeParam) + case "aws_iam": + cloudID, err = aws_cloud_id.GetCloudId() + case "gcp": + cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam) + default: + return "", fmt.Errorf("unable to determine provider: %s", provider) + } + return cloudID, err +} + +// readK8SServiceAccountJWT reads the JWT data for the Agent to submit to Akeyless Gateway. +func readK8SServiceAccountJWT() (string, error) { + data, err := os.Open(DefServiceAccountFile) + if err != nil { + return "", err + } + defer data.Close() + + contentBytes, err := ioutil.ReadAll(data) + if err != nil { + return "", err + } + + a := strings.TrimSpace(string(contentBytes)) + + return base64.StdEncoding.EncodeToString([]byte(a)), nil +} diff --git a/e2e/suite/alibaba/alibaba.go b/e2e/suite/alibaba/alibaba.go index e686c9788..a540ec8c5 100644 --- a/e2e/suite/alibaba/alibaba.go +++ b/e2e/suite/alibaba/alibaba.go @@ -31,7 +31,11 @@ var _ = Describe("[alibaba] ", func() { accessKeyID := os.Getenv("ACCESS_KEY_ID") accessKeySecret := os.Getenv("ACCESS_KEY_SECRET") regionID := os.Getenv("REGION_ID") - prov := newAlibabaProvider(f, accessKeyID, accessKeySecret, regionID) + prov := &alibabaProvider{} + + if accessKeyID != "" && accessKeySecret != "" && regionID != "" { + prov = newAlibabaProvider(f, accessKeyID, accessKeySecret, regionID) + } DescribeTable("sync secrets", framework.TableFunc(f, prov), Entry(common.SimpleDataSync(f)), diff --git a/e2e/suite/azure/azure.go b/e2e/suite/azure/azure.go index 6d15b8923..d1c57b1e8 100644 --- a/e2e/suite/azure/azure.go +++ b/e2e/suite/azure/azure.go @@ -30,7 +30,11 @@ var _ = Describe("[azure] ", func() { tenantID := os.Getenv("TENANT_ID") clientID := os.Getenv("AZURE_CLIENT_ID") clientSecret := os.Getenv("AZURE_CLIENT_SECRET") - prov := newazureProvider(f, clientID, clientSecret, tenantID, vaultURL) + prov := &azureProvider{} + + if vaultURL != "" && tenantID != "" && clientID != "" && clientSecret != "" { + prov = newazureProvider(f, clientID, clientSecret, tenantID, vaultURL) + } DescribeTable("sync secrets", framework.TableFunc(f, prov), Entry(common.SimpleDataSync(f)), diff --git a/e2e/suite/common/common.go b/e2e/suite/common/common.go index 144d3fb64..91206344e 100644 --- a/e2e/suite/common/common.go +++ b/e2e/suite/common/common.go @@ -22,6 +22,14 @@ import ( "github.com/external-secrets/external-secrets/e2e/framework" ) +const ( + // Constants. + dockerConfigExampleName = "docker-config-example" + dockerConfigJSONKey = ".dockerconfigjson" + mysecretToStringTemplating = "{{ .mysecret | toString }}" + sshPrivateKey = "ssh-privatekey" +) + // This case creates multiple secrets with simple key/value pairs and syncs them using multiple .Spec.Data blocks. // Not supported by: vault. func SimpleDataSync(f *framework.Framework) (string, func(*framework.TestCase)) { @@ -289,7 +297,7 @@ func NestedJSONWithGJSON(f *framework.Framework) (string, func(*framework.TestCa // not supported by: vault. func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase)) { return "[common] should sync docker configurated json secrets with template simple", func(tc *framework.TestCase) { - cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, "docker-config-example") + cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName) dockerconfig := `{"auths":{"https://index.docker.io/v1/": {"auth": "c3R...zE2"}}}` cloudSecretValue := fmt.Sprintf(`{"dockerconfig": %s}`, dockerconfig) tc.Secrets = map[string]string{ @@ -299,7 +307,7 @@ func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase) tc.ExpectedSecret = &v1.Secret{ Type: v1.SecretTypeOpaque, Data: map[string][]byte{ - ".dockerconfigjson": []byte(dockerconfig), + dockerConfigJSONKey: []byte(dockerconfig), }, } @@ -315,7 +323,7 @@ func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase) tc.ExternalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{ Data: map[string]string{ - ".dockerconfigjson": "{{ .mysecret | toString }}", + dockerConfigJSONKey: mysecretToStringTemplating, }, } } @@ -326,7 +334,7 @@ func DockerJSONConfig(f *framework.Framework) (string, func(*framework.TestCase) // Need to have a key holding dockerconfig to be supported by vault. func DataPropertyDockerconfigJSON(f *framework.Framework) (string, func(*framework.TestCase)) { return "[common] should sync docker configurated json secrets with template", func(tc *framework.TestCase) { - cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, "docker-config-example") + cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName) dockerconfigString := `"{\"auths\":{\"https://index.docker.io/v1/\": {\"auth\": \"c3R...zE2\"}}}"` dockerconfig := `{"auths":{"https://index.docker.io/v1/": {"auth": "c3R...zE2"}}}` cloudSecretValue := fmt.Sprintf(`{"dockerconfig": %s}`, dockerconfigString) @@ -337,7 +345,7 @@ func DataPropertyDockerconfigJSON(f *framework.Framework) (string, func(*framewo tc.ExpectedSecret = &v1.Secret{ Type: v1.SecretTypeDockerConfigJson, Data: map[string][]byte{ - ".dockerconfigjson": []byte(dockerconfig), + dockerConfigJSONKey: []byte(dockerconfig), }, } @@ -354,7 +362,7 @@ func DataPropertyDockerconfigJSON(f *framework.Framework) (string, func(*framewo tc.ExternalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{ Type: v1.SecretTypeDockerConfigJson, Data: map[string]string{ - ".dockerconfigjson": "{{ .mysecret | toString }}", + dockerConfigJSONKey: mysecretToStringTemplating, }, } } @@ -411,7 +419,7 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) { tc.ExpectedSecret = &v1.Secret{ Type: v1.SecretTypeSSHAuth, Data: map[string][]byte{ - "ssh-privatekey": []byte(sshSecretValue), + sshPrivateKey: []byte(sshSecretValue), }, } @@ -427,7 +435,7 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) { tc.ExternalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{ Type: v1.SecretTypeSSHAuth, Data: map[string]string{ - "ssh-privatekey": "{{ .mysecret | toString }}", + sshPrivateKey: mysecretToStringTemplating, }, } } @@ -436,7 +444,7 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) { // This case adds an ssh private key secret and syncs it. func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.TestCase)) { return "[common] should sync ssh key with provider.", func(tc *framework.TestCase) { - cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, "docker-config-example") + cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, dockerConfigExampleName) SSHKey := `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEAsARoZUqo6L5dd0WRjZ2QPq/kKlbjtUY1njzJ01UtdC1u1eSJFUnV @@ -483,7 +491,7 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes tc.ExpectedSecret = &v1.Secret{ Type: v1.SecretTypeSSHAuth, Data: map[string][]byte{ - "ssh-privatekey": []byte(SSHKey), + sshPrivateKey: []byte(SSHKey), }, } @@ -500,7 +508,7 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes tc.ExternalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{ Type: v1.SecretTypeSSHAuth, Data: map[string]string{ - "ssh-privatekey": "{{ .mysecret | toString }}", + sshPrivateKey: mysecretToStringTemplating, }, } } diff --git a/e2e/suite/gcp/gcp.go b/e2e/suite/gcp/gcp.go index b46ac8484..17e62165b 100644 --- a/e2e/suite/gcp/gcp.go +++ b/e2e/suite/gcp/gcp.go @@ -36,7 +36,11 @@ var _ = Describe("[gcp] ", func() { f := framework.New("eso-gcp") credentials := os.Getenv("GCP_SM_SA_JSON") projectID := os.Getenv("GCP_PROJECT_ID") - prov := newgcpProvider(f, credentials, projectID) + prov := &gcpProvider{} + + if credentials != "" && projectID != "" { + prov = newgcpProvider(f, credentials, projectID) + } // P12Cert case creates a secret with a p12 cert containing a privkey and cert bundled together. // It uses templating to generate a k8s secret of type tls with pem values diff --git a/e2e/suite/gitlab/gitlab.go b/e2e/suite/gitlab/gitlab.go index 03fbd824c..0f643eb84 100644 --- a/e2e/suite/gitlab/gitlab.go +++ b/e2e/suite/gitlab/gitlab.go @@ -33,7 +33,12 @@ var _ = Describe("[gitlab] ", func() { f := framework.New("esogitlab") credentials := os.Getenv("GITLAB_TOKEN") projectID := os.Getenv("GITLAB_PROJECT_ID") - prov := newGitlabProvider(f, credentials, projectID) + + prov := &gitlabProvider{} + + if credentials != "" && projectID != "" { + prov = newGitlabProvider(f, credentials, projectID) + } DescribeTable("sync secrets", framework.TableFunc(f, prov), Entry(common.SimpleDataSync(f)), diff --git a/e2e/suite/vault/vault.go b/e2e/suite/vault/vault.go index 2773a311f..56cb6fd50 100644 --- a/e2e/suite/vault/vault.go +++ b/e2e/suite/vault/vault.go @@ -23,6 +23,15 @@ import ( "github.com/external-secrets/external-secrets/e2e/suite/common" ) +const ( + withTokenAuth = "with token auth" + withCertAuth = "with cert auth" + withApprole = "with approle auth" + withV1 = "with v1 provider" + withJWT = "with jwt provider" + withK8s = "with kubernetes provider" +) + var _ = Describe("[vault] ", func() { f := framework.New("eso-vault") @@ -30,41 +39,41 @@ var _ = Describe("[vault] ", func() { framework.TableFunc(f, newVaultProvider(f)), // uses token auth - compose("with token auth", f, common.JSONDataFromSync, useTokenAuth), - compose("with token auth", f, common.JSONDataWithProperty, useTokenAuth), - compose("with token auth", f, common.JSONDataWithTemplate, useTokenAuth), - compose("with token auth", f, common.DataPropertyDockerconfigJSON, useTokenAuth), - compose("with token auth", f, common.JSONDataWithoutTargetName, useTokenAuth), + compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth), + compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth), + compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth), + compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth), + compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useTokenAuth), // use cert auth - compose("with cert auth", f, common.JSONDataFromSync, useCertAuth), - compose("with cert auth", f, common.JSONDataWithProperty, useCertAuth), - compose("with cert auth", f, common.JSONDataWithTemplate, useCertAuth), - compose("with cert auth", f, common.DataPropertyDockerconfigJSON, useCertAuth), - compose("with cert auth", f, common.JSONDataWithoutTargetName, useTokenAuth), + compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth), + compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth), + compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth), + compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth), + compose(withCertAuth, f, common.JSONDataWithoutTargetName, useTokenAuth), // use approle auth - compose("with appRole auth", f, common.JSONDataFromSync, useApproleAuth), - compose("with appRole auth", f, common.JSONDataWithProperty, useApproleAuth), - compose("with appRole auth", f, common.JSONDataWithTemplate, useApproleAuth), - compose("with appRole auth", f, common.DataPropertyDockerconfigJSON, useApproleAuth), - compose("with appRole auth", f, common.JSONDataWithoutTargetName, useTokenAuth), + compose(withApprole, f, common.JSONDataFromSync, useApproleAuth), + compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth), + compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth), + compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth), + compose(withApprole, f, common.JSONDataWithoutTargetName, useTokenAuth), // use v1 provider - compose("with v1 kv provider", f, common.JSONDataFromSync, useV1Provider), - compose("with v1 kv provider", f, common.JSONDataWithProperty, useV1Provider), - compose("with v1 kv provider", f, common.JSONDataWithTemplate, useV1Provider), - compose("with v1 kv provider", f, common.DataPropertyDockerconfigJSON, useV1Provider), - compose("with v1 kv provider", f, common.JSONDataWithoutTargetName, useTokenAuth), + compose(withV1, f, common.JSONDataFromSync, useV1Provider), + compose(withV1, f, common.JSONDataWithProperty, useV1Provider), + compose(withV1, f, common.JSONDataWithTemplate, useV1Provider), + compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider), + compose(withV1, f, common.JSONDataWithoutTargetName, useTokenAuth), // use jwt provider - compose("with jwt provider", f, common.JSONDataFromSync, useJWTProvider), - compose("with jwt provider", f, common.JSONDataWithProperty, useJWTProvider), - compose("with jwt provider", f, common.JSONDataWithTemplate, useJWTProvider), - compose("with jwt provider", f, common.DataPropertyDockerconfigJSON, useJWTProvider), - compose("with jwt provider", f, common.JSONDataWithoutTargetName, useTokenAuth), + compose(withJWT, f, common.JSONDataFromSync, useJWTProvider), + compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider), + compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider), + compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider), + compose(withJWT, f, common.JSONDataWithoutTargetName, useTokenAuth), // use kubernetes provider - compose("with kubernetes provider", f, common.JSONDataFromSync, useKubernetesProvider), - compose("with kubernetes provider", f, common.JSONDataWithProperty, useKubernetesProvider), - compose("with kubernetes provider", f, common.JSONDataWithTemplate, useKubernetesProvider), - compose("with kubernetes provider", f, common.DataPropertyDockerconfigJSON, useKubernetesProvider), - compose("with kubernetes provider", f, common.JSONDataWithoutTargetName, useTokenAuth), + compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider), + compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider), + compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider), + compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider), + compose(withK8s, f, common.JSONDataWithoutTargetName, useTokenAuth), ) }) diff --git a/go.mod b/go.mod index b29e2fa73..34b424093 100644 --- a/go.mod +++ b/go.mod @@ -33,20 +33,23 @@ replace ( ) require ( - cloud.google.com/go v0.65.0 + cloud.google.com/go v0.81.0 github.com/Azure/azure-sdk-for-go v54.1.0+incompatible github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/IBM/go-sdk-core/v5 v5.5.0 github.com/IBM/secrets-manager-go-sdk v1.0.23 + github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2 + github.com/akeylesslabs/akeyless-go/v2 v2.5.11 github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192 github.com/aws/aws-sdk-go v1.38.6 github.com/crossplane/crossplane-runtime v0.13.0 github.com/fatih/color v1.10.0 // indirect github.com/frankban/quicktest v1.10.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-logr/logr v0.4.0 - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v4 v4.2.0 github.com/google/go-cmp v0.5.5 github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.2.0 @@ -57,7 +60,7 @@ require ( github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4 github.com/kr/pretty v0.2.1 // indirect github.com/lestrrat-go/jwx v1.2.1 - github.com/onsi/ginkgo v1.16.4 + github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.16.0 github.com/oracle/oci-go-sdk/v45 v45.2.0 github.com/pierrec/lz4 v2.5.2+incompatible // indirect @@ -65,7 +68,7 @@ require ( github.com/prometheus/client_model v0.2.0 github.com/spf13/cobra v1.1.3 // indirect github.com/stretchr/testify v1.7.0 - github.com/tidwall/gjson v1.7.5 + github.com/tidwall/gjson v1.12.1 github.com/xanzy/go-gitlab v0.50.1 github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588 github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa @@ -73,11 +76,12 @@ require ( go.uber.org/zap v1.17.0 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect - golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c - golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect - google.golang.org/api v0.30.0 - google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a - google.golang.org/grpc v1.31.0 + golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78 + golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect + golang.org/x/tools v0.1.7 // indirect + google.golang.org/api v0.45.0 + google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3 + google.golang.org/grpc v1.37.0 honnef.co/go/tools v0.1.4 // indirect k8s.io/api v0.21.3 k8s.io/apimachinery v0.21.3 diff --git a/go.sum b/go.sum index a95d79455..5c28d4f17 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,13 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -72,6 +77,10 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2 h1:1h4udX3Y5KgSG0m4Th2bHfaYxZB9fbngiij9PrKEp6c= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2/go.mod h1:ionnWiARf5TYoFGuHS7syh51/7lYosZejpZbnECFJcU= +github.com/akeylesslabs/akeyless-go/v2 v2.5.11 h1:Z7xJAUPk1h4/YGS7yr/nx9RSMaSESzPwClqe21WFYHo= +github.com/akeylesslabs/akeyless-go/v2 v2.5.11/go.mod h1:uOdXD49NCCe4rexeSc2aBU5Qv4KZgJE6YlbtYalvb+I= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -92,6 +101,8 @@ github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.38.6 h1:h0AKIaz/A1kEJ50HxCv7tL1GW+KbxYbp75+lZ/nvFOI= github.com/aws/aws-sdk-go v1.38.6/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.23.0 h1:+E1q1LLSfHSDn/DzOtdJOX+pLZE2HiNV2yO5AjZINwM= +github.com/aws/aws-sdk-go-v2 v0.23.0/go.mod h1:2LhT7UgHOXK3UXONKI5OMgIyoQL6zTAw/jwIeX6yqzw= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -168,8 +179,9 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -210,6 +222,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= @@ -248,8 +261,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -265,6 +278,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 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= @@ -280,6 +294,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= @@ -294,6 +309,7 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -305,6 +321,7 @@ 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 v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -312,6 +329,10 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -394,6 +415,7 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:W github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -533,8 +555,9 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -645,13 +668,13 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tidwall/gjson v1.7.5 h1:zmAN/xmX7OtpAkv4Ovfso60r/BiCi5IErCDYGNJu+uc= -github.com/tidwall/gjson v1.7.5/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= -github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= -github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/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.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8= -github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -676,7 +699,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -687,8 +710,10 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -747,8 +772,9 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -758,6 +784,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB 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.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -800,19 +827,30 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c h1:HiAZXo96zOhVhtFHchj/ojzoxCFiPrp9/j0GtS38V3g= -golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78 h1:rPRtHfUb0UKZeZ6GH4K4Nt4YRbE9V1u+QZX5upZXqJQ= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -875,17 +913,26 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs= +golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= @@ -963,13 +1010,18 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 h1:cYSqdOzmV9wJ7lWurRAws06Dmif0Wv6UL4gQLlz+im0= -golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 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= @@ -993,8 +1045,14 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.45.0 h1:pqMffJFLBVUDIoYsHcqtxgQVTsmxMDpYLOc5MT4Jrww= +google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1033,9 +1091,20 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3 h1:K+7Ig5hjiLVA/i1UFUUbCGimWz5/Ey0lAQjT3QiLaPY= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml index 32c47f0a0..c51ea7352 100644 --- a/hack/api-docs/mkdocs.yml +++ b/hack/api-docs/mkdocs.yml @@ -45,6 +45,7 @@ nav: - Secrets Manager: provider-google-secrets-manager.md - IBM: - Secrets Manager: provider-ibm-secrets-manager.md + - Akeyless: provider-akeyless.md - HashiCorp Vault: provider-hashicorp-vault.md - Yandex: - Lockbox: provider-yandex-lockbox.md diff --git a/pkg/controllers/externalsecret/externalsecret_controller.go b/pkg/controllers/externalsecret/externalsecret_controller.go index 0bf318ed8..952ffd2eb 100644 --- a/pkg/controllers/externalsecret/externalsecret_controller.go +++ b/pkg/controllers/externalsecret/externalsecret_controller.go @@ -238,11 +238,16 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretReady, v1.ConditionTrue, esv1alpha1.ConditionReasonSecretSynced, "Secret was synced") + currCond := GetExternalSecretCondition(externalSecret.Status, esv1alpha1.ExternalSecretReady) SetExternalSecretCondition(&externalSecret, *conditionSynced) externalSecret.Status.RefreshTime = metav1.NewTime(time.Now()) externalSecret.Status.SyncedResourceVersion = getResourceVersion(externalSecret) syncCallsTotal.With(syncCallsMetricLabels).Inc() - log.V(1).Info("reconciled secret") + if currCond == nil || currCond.Status != conditionSynced.Status { + log.Info("reconciled secret") // Log once if on success in any verbosity + } else { + log.V(1).Info("reconciled secret") // Log all reconciliation cycles if higher verbosity applied + } return ctrl.Result{ RequeueAfter: refreshInt, diff --git a/pkg/controllers/externalsecret/externalsecret_controller_test.go b/pkg/controllers/externalsecret/externalsecret_controller_test.go index af3ab3b63..66019a78f 100644 --- a/pkg/controllers/externalsecret/externalsecret_controller_test.go +++ b/pkg/controllers/externalsecret/externalsecret_controller_test.go @@ -134,6 +134,11 @@ var _ = Describe("ExternalSecret controller", func() { ExternalSecretName = "test-es" ExternalSecretStore = "test-store" ExternalSecretTargetSecretName = "test-secret" + FakeManager = "fake.manager" + expectedSecretVal = "SOMEVALUE was templated" + targetPropObj = "{{ .targetProperty | toString | upper }} was templated" + FooValue = "map-foo-value" + BarValue = "map-bar-value" ) var ExternalSecretNamespace string @@ -283,13 +288,13 @@ var _ = Describe("ExternalSecret controller", func() { // create secret beforehand Expect(k8sClient.Create(context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: ExternalSecretTargetSecretName, Namespace: ExternalSecretNamespace, }, Data: map[string][]byte{ existingKey: []byte(existingVal), }, - }, client.FieldOwner("fake.manager"))).To(Succeed()) + }, client.FieldOwner(FakeManager))).To(Succeed()) fakeProvider.WithGetSecret([]byte(secretVal), nil) tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) { @@ -309,7 +314,7 @@ var _ = Describe("ExternalSecret controller", func() { "external-secrets", fmt.Sprintf("{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:%s\":{}}}}", esv1alpha1.AnnotationDataHash)), ).To(BeTrue()) - Expect(hasFieldOwnership(secret.ObjectMeta, "fake.manager", "{\"f:data\":{\".\":{},\"f:pre-existing-key\":{}},\"f:type\":{}}")).To(BeTrue()) + Expect(hasFieldOwnership(secret.ObjectMeta, FakeManager, "{\"f:data\":{\".\":{},\"f:pre-existing-key\":{}},\"f:type\":{}}")).To(BeTrue()) } } @@ -348,13 +353,13 @@ var _ = Describe("ExternalSecret controller", func() { // create secret beforehand Expect(k8sClient.Create(context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: ExternalSecretTargetSecretName, Namespace: ExternalSecretNamespace, }, Data: map[string][]byte{ existingKey: []byte(existingVal), }, - }, client.FieldOwner("fake.manager"))).To(Succeed()) + }, client.FieldOwner(FakeManager))).To(Succeed()) fakeProvider.WithGetSecret([]byte(secretVal), nil) tc.checkCondition = func(es *esv1alpha1.ExternalSecret) bool { @@ -373,7 +378,7 @@ var _ = Describe("ExternalSecret controller", func() { // check owner/managedFields Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse()) Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(1)) - Expect(hasFieldOwnership(secret.ObjectMeta, "fake.manager", "{\"f:data\":{\".\":{},\"f:targetProperty\":{}},\"f:type\":{}}")).To(BeTrue()) + Expect(hasFieldOwnership(secret.ObjectMeta, FakeManager, "{\"f:data\":{\".\":{},\"f:targetProperty\":{}},\"f:type\":{}}")).To(BeTrue()) } } @@ -381,7 +386,6 @@ var _ = Describe("ExternalSecret controller", func() { // to construct a new secret: labels, annotations and type syncWithTemplate := func(tc *testCase) { const secretVal = "someValue" - const expectedSecretVal = "SOMEVALUE was templated" const tplStaticKey = "tplstatickey" const tplStaticVal = "tplstaticvalue" tc.externalSecret.ObjectMeta.Labels = map[string]string{ @@ -401,7 +405,7 @@ var _ = Describe("ExternalSecret controller", func() { }, Type: v1.SecretTypeOpaque, Data: map[string]string{ - targetProp: "{{ .targetProperty | toString | upper }} was templated", + targetProp: targetPropObj, tplStaticKey: tplStaticVal, }, } @@ -426,7 +430,6 @@ var _ = Describe("ExternalSecret controller", func() { // * dataFrom syncWithTemplatePrecedence := func(tc *testCase) { const secretVal = "someValue" - const expectedSecretVal = "SOMEVALUE was templated" const tplStaticKey = "tplstatickey" const tplStaticVal = "tplstaticvalue" const tplFromCMName = "template-cm" @@ -480,7 +483,7 @@ var _ = Describe("ExternalSecret controller", func() { }, Data: map[string]string{ // this should be the data value, not dataFrom - targetProp: "{{ .targetProperty | toString | upper }} was templated", + targetProp: targetPropObj, // this should use the value from the map "bar": "value from map: {{ .bar | toString }}", // just a static value @@ -494,8 +497,8 @@ var _ = Describe("ExternalSecret controller", func() { } fakeProvider.WithGetSecret([]byte(secretVal), nil) fakeProvider.WithGetSecretMap(map[string][]byte{ - "targetProperty": []byte("map-foo-value"), - "bar": []byte("map-bar-value"), + "targetProperty": []byte(FooValue), + "bar": []byte(BarValue), }, nil) tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) { // check values @@ -509,7 +512,6 @@ var _ = Describe("ExternalSecret controller", func() { refreshWithTemplate := func(tc *testCase) { const secretVal = "someValue" - const expectedSecretVal = "SOMEVALUE was templated" const tplStaticKey = "tplstatickey" const tplStaticVal = "tplstaticvalue" tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second} @@ -520,7 +522,7 @@ var _ = Describe("ExternalSecret controller", func() { }, Type: v1.SecretTypeOpaque, Data: map[string]string{ - targetProp: "{{ .targetProperty | toString | upper }} was templated", + targetProp: targetPropObj, tplStaticKey: tplStaticVal, }, } @@ -660,13 +662,13 @@ var _ = Describe("ExternalSecret controller", func() { }, } fakeProvider.WithGetSecretMap(map[string][]byte{ - "foo": []byte("map-foo-value"), - "bar": []byte("map-bar-value"), + "foo": []byte(FooValue), + "bar": []byte(BarValue), }, nil) tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) { // check values - Expect(string(secret.Data["foo"])).To(Equal("map-foo-value")) - Expect(string(secret.Data["bar"])).To(Equal("map-bar-value")) + Expect(string(secret.Data["foo"])).To(Equal(FooValue)) + Expect(string(secret.Data["bar"])).To(Equal(BarValue)) } } @@ -687,14 +689,14 @@ var _ = Describe("ExternalSecret controller", func() { }, } fakeProvider.WithGetSecretMap(map[string][]byte{ - "tls.crt": []byte("map-foo-value"), - "tls.key": []byte("map-bar-value"), + "tls.crt": []byte(FooValue), + "tls.key": []byte(BarValue), }, nil) tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) { Expect(secret.Type).To(Equal(v1.SecretTypeTLS)) // check values - Expect(string(secret.Data["tls.crt"])).To(Equal("map-foo-value")) - Expect(string(secret.Data["tls.key"])).To(Equal("map-bar-value")) + Expect(string(secret.Data["tls.crt"])).To(Equal(FooValue)) + Expect(string(secret.Data["tls.key"])).To(Equal(BarValue)) } } @@ -851,7 +853,7 @@ var _ = Describe("ExternalSecret controller", func() { // When we amend the created kind=secret, refresh operation should be run again regardless of refresh interval checkSecretDataHashAnnotationChange := func(tc *testCase) { fakeData := map[string][]byte{ - "targetProperty": []byte("map-foo-value"), + "targetProperty": []byte(FooValue), } fakeProvider.WithGetSecretMap(fakeData, nil) tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Minute * 10} diff --git a/pkg/provider/akeyless/akeyless.go b/pkg/provider/akeyless/akeyless.go new file mode 100644 index 000000000..d3912a3ef --- /dev/null +++ b/pkg/provider/akeyless/akeyless.go @@ -0,0 +1,155 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package akeyless + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/akeylesslabs/akeyless-go/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" + "github.com/external-secrets/external-secrets/pkg/provider" + "github.com/external-secrets/external-secrets/pkg/provider/schema" + "github.com/external-secrets/external-secrets/pkg/utils" +) + +const ( + defaultAPIUrl = "https://api.akeyless.io" +) + +// Provider satisfies the provider interface. +type Provider struct{} + +// akeylessBase satisfies the provider.SecretsClient interface. +type akeylessBase struct { + kube client.Client + store esv1alpha1.GenericStore + namespace string + + akeylessGwAPIURL string + RestAPI *akeyless.V2ApiService +} + +type Akeyless struct { + Client akeylessVaultInterface +} + +type akeylessVaultInterface interface { + GetSecretByType(secretName, token string, version int32) (string, error) + TokenFromSecretRef(ctx context.Context) (string, error) +} + +func init() { + schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{ + Akeyless: &esv1alpha1.AkeylessProvider{}, + }) +} + +// NewClient constructs a new secrets client based on the provided store. +func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) { + return newClient(ctx, store, kube, namespace) +} + +func newClient(_ context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) { + akl := &akeylessBase{ + kube: kube, + store: store, + namespace: namespace, + } + + spec, err := GetAKeylessProvider(store) + if err != nil { + return nil, err + } + akeylessGwAPIURL := defaultAPIUrl + if spec != nil && spec.AkeylessGWApiURL != nil && *spec.AkeylessGWApiURL != "" { + akeylessGwAPIURL = getV2Url(*spec.AkeylessGWApiURL) + } + + if spec.Auth == nil { + return nil, fmt.Errorf("missing Auth in store config") + } + + RestAPIClient := akeyless.NewAPIClient(&akeyless.Configuration{ + Servers: []akeyless.ServerConfiguration{ + { + URL: akeylessGwAPIURL, + }, + }, + }).V2Api + + akl.akeylessGwAPIURL = akeylessGwAPIURL + akl.RestAPI = RestAPIClient + return &Akeyless{Client: akl}, nil +} + +func (a *Akeyless) Close(ctx context.Context) error { + return nil +} + +// Implements store.Client.GetSecret Interface. +// Retrieves a secret with the secret name defined in ref.Name. +func (a *Akeyless) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + if utils.IsNil(a.Client) { + return nil, fmt.Errorf(errUninitalizedAkeylessProvider) + } + + token, err := a.Client.TokenFromSecretRef(ctx) + if err != nil { + return nil, err + } + version := int32(0) + if ref.Version != "" { + i, err := strconv.ParseInt(ref.Version, 10, 32) + if err == nil { + version = int32(i) + } + } + value, err := a.Client.GetSecretByType(ref.Key, token, version) + if err != nil { + return nil, err + } + return []byte(value), nil +} + +// Implements store.Client.GetSecretMap Interface. +// New version of GetSecretMap. +func (a *Akeyless) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + if utils.IsNil(a.Client) { + return nil, fmt.Errorf(errUninitalizedAkeylessProvider) + } + + val, err := a.GetSecret(ctx, ref) + if err != nil { + return nil, err + } + // Maps the json data to a string:string map + kv := make(map[string]string) + err = json.Unmarshal(val, &kv) + if err != nil { + return nil, fmt.Errorf(errJSONSecretUnmarshal, err) + } + + // Converts values in K:V pairs into bytes, while leaving keys as strings + secretData := make(map[string][]byte) + for k, v := range kv { + secretData[k] = []byte(v) + } + return secretData, nil +} diff --git a/pkg/provider/akeyless/akeyless_api.go b/pkg/provider/akeyless/akeyless_api.go new file mode 100644 index 000000000..f4a5b0400 --- /dev/null +++ b/pkg/provider/akeyless/akeyless_api.go @@ -0,0 +1,250 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package akeyless + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure" + gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp" + "github.com/akeylesslabs/akeyless-go/v2" +) + +var apiErr akeyless.GenericOpenAPIError + +const DefServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + +func (a *akeylessBase) GetToken(accessID, accType, accTypeParam string) (string, error) { + ctx := context.Background() + authBody := akeyless.NewAuthWithDefaults() + authBody.AccessId = akeyless.PtrString(accessID) + if accType == "api_key" || accType == "access_key" { + authBody.AccessKey = akeyless.PtrString(accTypeParam) + } else if accType == "k8s" { + jwtString, err := readK8SServiceAccountJWT() + if err != nil { + return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err) + } + K8SAuthConfigName := accTypeParam + authBody.AccessType = akeyless.PtrString(accType) + authBody.K8sServiceAccountToken = akeyless.PtrString(jwtString) + authBody.K8sAuthConfigName = akeyless.PtrString(K8SAuthConfigName) + } else { + cloudID, err := a.getCloudID(accType, accTypeParam) + if err != nil { + return "", fmt.Errorf("Require Cloud ID " + err.Error()) + } + authBody.AccessType = akeyless.PtrString(accType) + authBody.CloudId = akeyless.PtrString(cloudID) + } + + authOut, _, err := a.RestAPI.Auth(ctx).Body(*authBody).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("authentication failed: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("authentication failed: %w", err) + } + + token := authOut.GetToken() + return token, nil +} + +func (a *akeylessBase) GetSecretByType(secretName, token string, version int32) (string, error) { + item, err := a.DescribeItem(secretName, token) + if err != nil { + return "", err + } + secretType := item.GetItemType() + + switch secretType { + case "STATIC_SECRET": + return a.GetStaticSecret(secretName, token, version) + case "DYNAMIC_SECRET": + return a.GetDynamicSecrets(secretName, token) + case "ROTATED_SECRET": + return a.GetRotatedSecrets(secretName, token, version) + default: + return "", fmt.Errorf("invalid item type: %v", secretType) + } +} + +func (a *akeylessBase) DescribeItem(itemName, token string) (*akeyless.Item, error) { + ctx := context.Background() + + body := akeyless.DescribeItem{ + Name: itemName, + } + if strings.HasPrefix(token, "u-") { + body.UidToken = &token + } else { + body.Token = &token + } + gsvOut, _, err := a.RestAPI.DescribeItem(ctx).Body(body).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return nil, fmt.Errorf("can't describe item: %v", string(apiErr.Body())) + } + return nil, fmt.Errorf("can't describe item: %w", err) + } + + return &gsvOut, nil +} + +func (a *akeylessBase) GetRotatedSecrets(secretName, token string, version int32) (string, error) { + ctx := context.Background() + + body := akeyless.GetRotatedSecretValue{ + Names: secretName, + Version: &version, + } + if strings.HasPrefix(token, "u-") { + body.UidToken = &token + } else { + body.Token = &token + } + + gsvOut, _, err := a.RestAPI.GetRotatedSecretValue(ctx).Body(body).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("can't get rotated secret value: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("can't get rotated secret value: %w", err) + } + + val, ok := gsvOut["value"] + if ok { + if _, ok := val["payload"]; ok { + return fmt.Sprintf("%v", val["payload"]), nil + } else if _, ok := val["target_value"]; ok { + out, err := json.Marshal(val["target_value"]) + if err != nil { + return "", fmt.Errorf("can't marshal rotated secret value: %w", err) + } + return string(out), nil + } else { + out, err := json.Marshal(val) + if err != nil { + return "", fmt.Errorf("can't marshal rotated secret value: %w", err) + } + return string(out), nil + } + } + out, err := json.Marshal(gsvOut) + if err != nil { + return "", fmt.Errorf("can't marshal rotated secret value: %w", err) + } + return string(out), nil +} + +func (a *akeylessBase) GetDynamicSecrets(secretName, token string) (string, error) { + ctx := context.Background() + + body := akeyless.GetDynamicSecretValue{ + Name: secretName, + } + if strings.HasPrefix(token, "u-") { + body.UidToken = &token + } else { + body.Token = &token + } + + gsvOut, _, err := a.RestAPI.GetDynamicSecretValue(ctx).Body(body).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("can't get dynamic secret value: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("can't get dynamic secret value: %w", err) + } + + out, err := json.Marshal(gsvOut) + if err != nil { + return "", fmt.Errorf("can't marshal dynamic secret value: %w", err) + } + + return string(out), nil +} + +func (a *akeylessBase) GetStaticSecret(secretName, token string, version int32) (string, error) { + ctx := context.Background() + + gsvBody := akeyless.GetSecretValue{ + Names: []string{secretName}, + Version: &version, + } + + if strings.HasPrefix(token, "u-") { + gsvBody.UidToken = &token + } else { + gsvBody.Token = &token + } + + gsvOut, _, err := a.RestAPI.GetSecretValue(ctx).Body(gsvBody).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("can't get secret value: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("can't get secret value: %w", err) + } + val, ok := gsvOut[secretName] + if !ok { + return "", fmt.Errorf("can't get secret: %v", secretName) + } + + return val, nil +} + +func (a *akeylessBase) getCloudID(provider, accTypeParam string) (string, error) { + var cloudID string + var err error + + switch provider { + case "azure_ad": + cloudID, err = azure_cloud_id.GetCloudId(accTypeParam) + case "aws_iam": + cloudID, err = aws_cloud_id.GetCloudId() + case "gcp": + cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam) + default: + return "", fmt.Errorf("unable to determine provider: %s", provider) + } + return cloudID, err +} + +// readK8SServiceAccountJWT reads the JWT data for the Agent to submit to Akeyless Gateway. +func readK8SServiceAccountJWT() (string, error) { + data, err := os.Open(DefServiceAccountFile) + if err != nil { + return "", err + } + defer data.Close() + + contentBytes, err := ioutil.ReadAll(data) + if err != nil { + return "", err + } + + a := strings.TrimSpace(string(contentBytes)) + + return base64.StdEncoding.EncodeToString([]byte(a)), nil +} diff --git a/pkg/provider/akeyless/akeyless_test.go b/pkg/provider/akeyless/akeyless_test.go new file mode 100644 index 000000000..8b1585d02 --- /dev/null +++ b/pkg/provider/akeyless/akeyless_test.go @@ -0,0 +1,168 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package akeyless + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" + fakeakeyless "github.com/external-secrets/external-secrets/pkg/provider/akeyless/fake" +) + +type akeylessTestCase struct { + mockClient *fakeakeyless.AkeylessMockClient + apiInput *fakeakeyless.Input + apiOutput *fakeakeyless.Output + ref *esv1alpha1.ExternalSecretDataRemoteRef + expectError string + expectedSecret string + // for testing secretmap + expectedData map[string][]byte +} + +func makeValidAkeylessTestCase() *akeylessTestCase { + smtc := akeylessTestCase{ + mockClient: &fakeakeyless.AkeylessMockClient{}, + apiInput: makeValidInput(), + ref: makeValidRef(), + apiOutput: makeValidOutput(), + expectError: "", + expectedSecret: "", + expectedData: map[string][]byte{}, + } + smtc.mockClient.WithValue(smtc.apiInput, smtc.apiOutput) + return &smtc +} + +func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef { + return &esv1alpha1.ExternalSecretDataRemoteRef{ + Key: "test-secret", + Version: "1", + } +} + +func makeValidInput() *fakeakeyless.Input { + return &fakeakeyless.Input{ + SecretName: "name", + Version: 0, + Token: "token", + } +} + +func makeValidOutput() *fakeakeyless.Output { + return &fakeakeyless.Output{ + Value: "secret-val", + Err: nil, + } +} + +func makeValidAkeylessTestCaseCustom(tweaks ...func(smtc *akeylessTestCase)) *akeylessTestCase { + smtc := makeValidAkeylessTestCase() + for _, fn := range tweaks { + fn(smtc) + } + smtc.mockClient.WithValue(smtc.apiInput, smtc.apiOutput) + return smtc +} + +// This case can be shared by both GetSecret and GetSecretMap tests. +// bad case: set apiErr. +var setAPIErr = func(smtc *akeylessTestCase) { + smtc.apiOutput.Err = fmt.Errorf("oh no") + smtc.expectError = "oh no" +} + +var setNilMockClient = func(smtc *akeylessTestCase) { + smtc.mockClient = nil + smtc.expectError = errUninitalizedAkeylessProvider +} + +func TestAkeylessGetSecret(t *testing.T) { + secretValue := "changedvalue" + // good case: default version is set + // key is passed in, output is sent back + setSecretString := func(smtc *akeylessTestCase) { + smtc.apiOutput = &fakeakeyless.Output{ + Value: secretValue, + Err: nil, + } + smtc.expectedSecret = secretValue + } + + successCases := []*akeylessTestCase{ + makeValidAkeylessTestCaseCustom(setAPIErr), + makeValidAkeylessTestCaseCustom(setSecretString), + makeValidAkeylessTestCaseCustom(setNilMockClient), + } + + sm := Akeyless{} + for k, v := range successCases { + sm.Client = v.mockClient + fmt.Println(*v.ref) + out, err := sm.GetSecret(context.Background(), *v.ref) + if !ErrorContains(err, v.expectError) { + t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError) + } + if string(out) != v.expectedSecret { + t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out)) + } + } +} + +func TestGetSecretMap(t *testing.T) { + // good case: default version & deserialization + setDeserialization := func(smtc *akeylessTestCase) { + smtc.apiOutput.Value = `{"foo":"bar"}` + smtc.expectedData["foo"] = []byte("bar") + } + + // bad case: invalid json + setInvalidJSON := func(smtc *akeylessTestCase) { + smtc.apiOutput.Value = `-----------------` + smtc.expectError = "unable to unmarshal secret" + } + + successCases := []*akeylessTestCase{ + makeValidAkeylessTestCaseCustom(setDeserialization), + makeValidAkeylessTestCaseCustom(setInvalidJSON), + makeValidAkeylessTestCaseCustom(setAPIErr), + makeValidAkeylessTestCaseCustom(setNilMockClient), + } + + sm := Akeyless{} + for k, v := range successCases { + sm.Client = v.mockClient + out, err := sm.GetSecretMap(context.Background(), *v.ref) + if !ErrorContains(err, v.expectError) { + t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError) + } + if err == nil && !reflect.DeepEqual(out, v.expectedData) { + t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out) + } + } +} + +func ErrorContains(out error, want string) bool { + if out == nil { + return want == "" + } + if want == "" { + return false + } + return strings.Contains(out.Error(), want) +} diff --git a/pkg/provider/akeyless/auth.go b/pkg/provider/akeyless/auth.go new file mode 100644 index 000000000..f28465c3a --- /dev/null +++ b/pkg/provider/akeyless/auth.go @@ -0,0 +1,103 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package akeyless + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" +) + +const ( + errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterSecretStore: missing Akeyless AccessID Namespace" + errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing Akeyless AccessType Namespace" + errFetchAKIDSecret = "could not fetch accessID secret: %w" + errFetchSAKSecret = "could not fetch AccessType secret: %w" + errMissingSAK = "missing SecretAccessKey" + errMissingAKID = "missing AccessKeyID" +) + +func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) { + prov, err := GetAKeylessProvider(a.store) + if err != nil { + return "", err + } + + ke := client.ObjectKey{ + Name: prov.Auth.SecretRef.AccessID.Name, + Namespace: a.namespace, // default to ExternalSecret namespace + } + // only ClusterStore is allowed to set namespace (and then it's required) + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.SecretRef.AccessID.Namespace == nil { + return "", fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace) + } + ke.Namespace = *prov.Auth.SecretRef.AccessID.Namespace + } + accessIDSecret := v1.Secret{} + err = a.kube.Get(ctx, ke, &accessIDSecret) + if err != nil { + return "", fmt.Errorf(errFetchAKIDSecret, err) + } + ke = client.ObjectKey{ + Name: prov.Auth.SecretRef.AccessType.Name, + Namespace: a.namespace, // default to ExternalSecret namespace + } + // only ClusterStore is allowed to set namespace (and then it's required) + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.SecretRef.AccessType.Namespace == nil { + return "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace) + } + ke.Namespace = *prov.Auth.SecretRef.AccessType.Namespace + } + accessTypeSecret := v1.Secret{} + err = a.kube.Get(ctx, ke, &accessTypeSecret) + if err != nil { + return "", fmt.Errorf(errFetchSAKSecret, err) + } + + ke = client.ObjectKey{ + Name: prov.Auth.SecretRef.AccessTypeParam.Name, + Namespace: a.namespace, // default to ExternalSecret namespace + } + // only ClusterStore is allowed to set namespace (and then it's required) + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.SecretRef.AccessType.Namespace == nil { + return "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace) + } + ke.Namespace = *prov.Auth.SecretRef.AccessType.Namespace + } + accessTypeParamSecret := v1.Secret{} + err = a.kube.Get(ctx, ke, &accessTypeParamSecret) + if err != nil { + return "", fmt.Errorf(errFetchSAKSecret, err) + } + accessID := string(accessIDSecret.Data[prov.Auth.SecretRef.AccessID.Key]) + accessType := string(accessTypeSecret.Data[prov.Auth.SecretRef.AccessType.Key]) + accessTypeParam := string(accessTypeSecret.Data[prov.Auth.SecretRef.AccessTypeParam.Key]) + + if accessID == "" { + return "", fmt.Errorf(errMissingSAK) + } + if accessType == "" { + return "", fmt.Errorf(errMissingAKID) + } + + return a.GetToken(accessID, accessType, accessTypeParam) +} diff --git a/pkg/provider/akeyless/fake/fake.go b/pkg/provider/akeyless/fake/fake.go new file mode 100644 index 000000000..f93185b8e --- /dev/null +++ b/pkg/provider/akeyless/fake/fake.go @@ -0,0 +1,49 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package fake + +import ( + "context" +) + +type AkeylessMockClient struct { + getSecret func(secretName, token string, version int32) (string, error) +} + +func (mc *AkeylessMockClient) TokenFromSecretRef(ctx context.Context) (string, error) { + return "newToken", nil +} + +func (mc *AkeylessMockClient) GetSecretByType(secretName, token string, version int32) (string, error) { + return mc.getSecret(secretName, token, version) +} + +func (mc *AkeylessMockClient) WithValue(in *Input, out *Output) { + if mc != nil { + mc.getSecret = func(secretName, token string, version int32) (string, error) { + return out.Value, out.Err + } + } +} + +type Input struct { + SecretName string + Token string + Version int32 +} + +type Output struct { + Value string + Err error +} diff --git a/pkg/provider/akeyless/utils.go b/pkg/provider/akeyless/utils.go new file mode 100644 index 000000000..a6d52de06 --- /dev/null +++ b/pkg/provider/akeyless/utils.go @@ -0,0 +1,99 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package akeyless + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" +) + +const ( + errNilStore = "found nil store" + errMissingStoreSpec = "store is missing spec" + errMissingProvider = "storeSpec is missing provider" + errInvalidProvider = "invalid provider spec. Missing Akeyless field in store %s" + errJSONSecretUnmarshal = "unable to unmarshal secret: %w" + errUninitalizedAkeylessProvider = "provider akeyless is not initialized" +) + +// GetAKeylessProvider does the necessary nil checks and returns the akeyless provider or an error. +func GetAKeylessProvider(store esv1alpha1.GenericStore) (*esv1alpha1.AkeylessProvider, error) { + if store == nil { + return nil, fmt.Errorf(errNilStore) + } + spc := store.GetSpec() + if spc == nil { + return nil, fmt.Errorf(errMissingStoreSpec) + } + if spc.Provider == nil { + return nil, fmt.Errorf(errMissingProvider) + } + prov := spc.Provider.Akeyless + if prov == nil { + return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String()) + } + return prov, nil +} + +func getV2Url(path string) string { + // add check if not v2 + rebody := sendReq(path) + if strings.Contains(rebody, "unknown command") { + return path + } + + if strings.HasSuffix(path, "/v2") { + return path + } + url, err := url.Parse(path) + if err != nil { + return path + } + if strings.HasSuffix(url.Host, "/v2") { + return path + } + url.Host += "/v2" + p := url.Scheme + "://" + url.Host + if url.Port() != "" { + p = p + ":" + url.Port() + } + + return p +} + +func sendReq(url string) string { + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return "" + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 10 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + return string(body) +} diff --git a/pkg/provider/aws/auth/auth.go b/pkg/provider/aws/auth/auth.go index 187afaa45..4b7226b20 100644 --- a/pkg/provider/aws/auth/auth.go +++ b/pkg/provider/aws/auth/auth.go @@ -160,6 +160,9 @@ func sessionFromSecretRef(ctx context.Context, prov *esv1alpha1.AWSProvider, sto func sessionFromServiceAccount(ctx context.Context, prov *esv1alpha1.AWSProvider, store esv1alpha1.GenericStore, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) { if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.JWTAuth.ServiceAccountRef.Namespace == nil { + return nil, fmt.Errorf("serviceAccountRef has no Namespace field (mandatory for ClusterSecretStore specs)") + } namespace = *prov.Auth.JWTAuth.ServiceAccountRef.Namespace } name := prov.Auth.JWTAuth.ServiceAccountRef.Name diff --git a/pkg/provider/aws/auth/auth_test.go b/pkg/provider/aws/auth/auth_test.go index 3f81f63de..0171cb824 100644 --- a/pkg/provider/aws/auth/auth_test.go +++ b/pkg/provider/aws/auth/auth_test.go @@ -38,16 +38,13 @@ import ( ) const ( - myServiceAcc = "my-service-account" - otherNs = "other-ns" + esNamespaceKey = "es-namespace" + platformTeamNsKey = "platform-team-ns" + myServiceAccountKey = "my-service-account" + otherNsName = "other-ns" ) func TestNewSession(t *testing.T) { - const ( - esNamespace = "es-namespace" - platformTeamNs = "platform-team-ns" - ) - rows := []TestSessionRow{ { name: "nil store", @@ -271,7 +268,7 @@ func TestNewSession(t *testing.T) { }, { name: "ClusterStore should use credentials from a specific namespace", - namespace: esNamespace, + namespace: esNamespaceKey, store: &esv1alpha1.ClusterSecretStore{ TypeMeta: metav1.TypeMeta{ APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion, @@ -284,12 +281,12 @@ func TestNewSession(t *testing.T) { SecretRef: &esv1alpha1.AWSAuthSecretRef{ AccessKeyID: esmeta.SecretKeySelector{ Name: "onesecret", - Namespace: aws.String(platformTeamNs), + Namespace: aws.String(platformTeamNsKey), Key: "one", }, SecretAccessKey: esmeta.SecretKeySelector{ Name: "onesecret", - Namespace: aws.String(platformTeamNs), + Namespace: aws.String(platformTeamNsKey), Key: "two", }, }, @@ -302,7 +299,7 @@ func TestNewSession(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Name: "onesecret", - Namespace: platformTeamNs, + Namespace: platformTeamNsKey, }, Data: map[string][]byte{ "one": []byte("1111"), @@ -316,7 +313,7 @@ func TestNewSession(t *testing.T) { }, { name: "namespace is mandatory when using ClusterStore with SecretKeySelector", - namespace: esNamespace, + namespace: esNamespaceKey, store: &esv1alpha1.ClusterSecretStore{ TypeMeta: metav1.TypeMeta{ APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion, @@ -345,19 +342,19 @@ func TestNewSession(t *testing.T) { }, { name: "jwt auth via cluster secret store", - namespace: esNamespace, + namespace: esNamespaceKey, sa: &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ - Name: myServiceAcc, - Namespace: otherNs, + Name: myServiceAccountKey, + Namespace: otherNsName, Annotations: map[string]string{ roleARNAnnotation: "my-sa-role", }, }, }, jwtProvider: func(name, namespace, roleArn, region string) (credentials.Provider, error) { - assert.Equal(t, myServiceAcc, name) - assert.Equal(t, otherNs, namespace) + assert.Equal(t, myServiceAccountKey, name) + assert.Equal(t, otherNsName, namespace) assert.Equal(t, "my-sa-role", roleArn) return fakesess.CredentialsProvider{ RetrieveFunc: func() (credentials.Value, error) { @@ -382,8 +379,8 @@ func TestNewSession(t *testing.T) { Auth: esv1alpha1.AWSAuth{ JWTAuth: &esv1alpha1.AWSJWTAuth{ ServiceAccountRef: &esmeta.ServiceAccountSelector{ - Name: myServiceAcc, - Namespace: aws.String(otherNs), + Name: myServiceAccountKey, + Namespace: aws.String(otherNsName), }, }, }, @@ -434,8 +431,8 @@ func testRow(t *testing.T, row TestSessionRow) { } err := kc.Create(context.Background(), &authv1.TokenRequest{ ObjectMeta: metav1.ObjectMeta{ - Name: myServiceAcc, - Namespace: otherNs, + Name: myServiceAccountKey, + Namespace: otherNsName, }, }) assert.Nil(t, err) diff --git a/pkg/provider/azure/keyvault/keyvault.go b/pkg/provider/azure/keyvault/keyvault.go index 94c9cd3b2..0912cce0d 100644 --- a/pkg/provider/azure/keyvault/keyvault.go +++ b/pkg/provider/azure/keyvault/keyvault.go @@ -35,6 +35,7 @@ import ( const ( defaultObjType = "secret" + vaultResource = "https://vault.azure.net" ) // Provider satisfies the provider interface. @@ -74,15 +75,18 @@ func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.C store: store, namespace: namespace, } - azClient, vaultURL, err := anAzure.newAzureClient(ctx) - if err != nil { - return nil, err + clientSet, err := anAzure.setAzureClientWithManagedIdentity() + if clientSet { + return anAzure, err } - anAzure.baseClient = azClient - anAzure.vaultURL = vaultURL - return anAzure, nil + clientSet, err = anAzure.setAzureClientWithServicePrincipal(ctx) + if clientSet { + return anAzure, err + } + + return nil, fmt.Errorf("cannot initialize Azure Client: no valid authType was specified") } // Implements store.Client.GetSecret Interface. @@ -168,42 +172,75 @@ func (a *Azure) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretD return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName) } -func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, string, error) { +func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) { spec := *a.store.GetSpec().Provider.AzureKV - tenantID := *spec.TenantID - vaultURL := *spec.VaultURL - if spec.AuthSecretRef == nil { - return nil, "", fmt.Errorf("missing clientID/clientSecret in store config") - } - clusterScoped := false - if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { - clusterScoped = true - } - if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil { - return nil, "", fmt.Errorf("missing accessKeyID/secretAccessKey in store config") - } - cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped) - if err != nil { - return nil, "", err - } - csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped) - if err != nil { - return nil, "", err + if *spec.AuthType != esv1alpha1.ManagedIdentity { + return false, nil } - clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, tenantID) - // the default resource api is the management URL and not the vault URL which we need for keyvault operations - clientCredentialsConfig.Resource = "https://vault.azure.net" - authorizer, err := clientCredentialsConfig.Authorizer() + msiConfig := kvauth.NewMSIConfig() + msiConfig.Resource = vaultResource + if spec.IdentityID != nil { + msiConfig.ClientID = *spec.IdentityID + } + authorizer, err := msiConfig.Authorizer() if err != nil { - return nil, "", err + return true, err } basicClient := keyvault.New() basicClient.Authorizer = authorizer - return &basicClient, vaultURL, nil + a.baseClient = basicClient + a.vaultURL = *spec.VaultURL + + return true, nil +} + +func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) { + spec := *a.store.GetSpec().Provider.AzureKV + + if *spec.AuthType != esv1alpha1.ServicePrincipal { + return false, nil + } + + if spec.TenantID == nil { + return true, fmt.Errorf("missing tenantID in store config") + } + if spec.AuthSecretRef == nil { + return true, fmt.Errorf("missing clientID/clientSecret in store config") + } + if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil { + return true, fmt.Errorf("missing accessKeyID/secretAccessKey in store config") + } + clusterScoped := false + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + clusterScoped = true + } + cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped) + if err != nil { + return true, err + } + csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped) + if err != nil { + return true, err + } + + clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *spec.TenantID) + clientCredentialsConfig.Resource = vaultResource + authorizer, err := clientCredentialsConfig.Authorizer() + if err != nil { + return true, err + } + + basicClient := keyvault.New() + basicClient.Authorizer = authorizer + + a.baseClient = &basicClient + a.vaultURL = *spec.VaultURL + + return true, nil } func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) { diff --git a/pkg/provider/azure/keyvault/keyvault_test.go b/pkg/provider/azure/keyvault/keyvault_test.go index b79311fa5..c5cddf268 100644 --- a/pkg/provider/azure/keyvault/keyvault_test.go +++ b/pkg/provider/azure/keyvault/keyvault_test.go @@ -39,15 +39,46 @@ func newAzure() (Azure, *fake.AzureMock) { return testAzure, azureMock } -func TestNewClientNoCreds(t *testing.T) { +func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) { namespace := "internal" vaultURL := "https://local.vault.url" - tenantID := "1234" + identityID := "1234" + authType := esv1alpha1.ManagedIdentity store := esv1alpha1.SecretStore{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, }, Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{ + AuthType: &authType, + IdentityID: &identityID, + VaultURL: &vaultURL, + }}}, + } + + provider, err := schema.GetProvider(&store) + tassert.Nil(t, err, "the return err should be nil") + k8sClient := clientfake.NewClientBuilder().Build() + secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace) + if err != nil { + // On non Azure environment, MSI auth not available, so this error should be returned + tassert.EqualError(t, err, "failed to get oauth token from MSI: MSI not available") + } else { + // On Azure (where GitHub Actions are running) a secretClient is returned, as only an Authorizer is configured, but no token is requested for MI + tassert.NotNil(t, secretClient) + } +} + +func TestNewClientNoCreds(t *testing.T) { + namespace := "internal" + vaultURL := "https://local.vault.url" + tenantID := "1234" + authType := esv1alpha1.ServicePrincipal + store := esv1alpha1.SecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{ + AuthType: &authType, VaultURL: &vaultURL, TenantID: &tenantID, }}}, @@ -55,32 +86,27 @@ func TestNewClientNoCreds(t *testing.T) { provider, err := schema.GetProvider(&store) tassert.Nil(t, err, "the return err should be nil") k8sClient := clientfake.NewClientBuilder().Build() - secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace) + _, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) tassert.EqualError(t, err, "missing clientID/clientSecret in store config") - tassert.Nil(t, secretClient) store.Spec.Provider.AzureKV.AuthSecretRef = &esv1alpha1.AzureKVAuth{} - secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) + _, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config") - tassert.Nil(t, secretClient) store.Spec.Provider.AzureKV.AuthSecretRef.ClientID = &v1.SecretKeySelector{Name: "user"} - secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) + _, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config") - tassert.Nil(t, secretClient) store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret = &v1.SecretKeySelector{Name: "password"} - secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) + _, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) tassert.EqualError(t, err, "could not find secret internal/user: secrets \"user\" not found") - tassert.Nil(t, secretClient) store.TypeMeta.Kind = esv1alpha1.ClusterSecretStoreKind store.TypeMeta.APIVersion = esv1alpha1.ClusterSecretStoreKindAPIVersion ns := "default" store.Spec.Provider.AzureKV.AuthSecretRef.ClientID.Namespace = &ns store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret.Namespace = &ns - secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) + _, err = provider.NewClient(context.Background(), &store, k8sClient, namespace) tassert.EqualError(t, err, "could not find secret default/user: secrets \"user\" not found") - tassert.Nil(t, secretClient) } const ( diff --git a/pkg/provider/ibm/provider.go b/pkg/provider/ibm/provider.go index 379a85c41..d4126e8bc 100644 --- a/pkg/provider/ibm/provider.go +++ b/pkg/provider/ibm/provider.go @@ -109,82 +109,102 @@ func (ibm *providerIBM) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSe switch secretType { case sm.GetSecretOptionsSecretTypeArbitraryConst: - response, _, err := ibm.IBMClient.GetSecret( - &sm.GetSecretOptions{ - SecretType: core.StringPtr(sm.GetSecretOptionsSecretTypeArbitraryConst), - ID: &secretName, - }) - if err != nil { - return nil, err - } - secret := response.Resources[0].(*sm.SecretResource) - secretData := secret.SecretData.(map[string]interface{}) - arbitrarySecretPayload := secretData["payload"].(string) - return []byte(arbitrarySecretPayload), nil + return getArbitrarySecret(ibm, &secretName) case sm.CreateSecretOptionsSecretTypeUsernamePasswordConst: + if ref.Property == "" { return nil, fmt.Errorf("remoteRef.property required for secret type username_password") } - response, _, err := ibm.IBMClient.GetSecret( - &sm.GetSecretOptions{ - SecretType: core.StringPtr(sm.CreateSecretOptionsSecretTypeUsernamePasswordConst), - ID: &secretName, - }) - if err != nil { - return nil, err - } - - secret := response.Resources[0].(*sm.SecretResource) - secretData := secret.SecretData.(map[string]interface{}) - - if val, ok := secretData[ref.Property]; ok { - return []byte(val.(string)), nil - } - return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key) + return getUsernamePasswordSecret(ibm, &secretName, ref) case sm.CreateSecretOptionsSecretTypeIamCredentialsConst: - response, _, err := ibm.IBMClient.GetSecret( - &sm.GetSecretOptions{ - SecretType: core.StringPtr(sm.CreateSecretOptionsSecretTypeIamCredentialsConst), - ID: &secretName, - }) - if err != nil { - return nil, err - } - secret := response.Resources[0].(*sm.SecretResource) - secretData := *secret.APIKey - - return []byte(secretData), nil + return getIamCredentialsSecret(ibm, &secretName) case sm.CreateSecretOptionsSecretTypeImportedCertConst: + if ref.Property == "" { return nil, fmt.Errorf("remoteRef.property required for secret type imported_cert") } - response, _, err := ibm.IBMClient.GetSecret( - &sm.GetSecretOptions{ - SecretType: core.StringPtr(sm.CreateSecretOptionsSecretTypeImportedCertConst), - ID: &secretName, - }) - if err != nil { - return nil, err - } - - secret := response.Resources[0].(*sm.SecretResource) - secretData := secret.SecretData.(map[string]interface{}) - - if val, ok := secretData[ref.Property]; ok { - return []byte(val.(string)), nil - } - return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key) + return getImportCertSecret(ibm, &secretName, ref) default: return nil, fmt.Errorf("unknown secret type %s", secretType) } } +func getArbitrarySecret(ibm *providerIBM, secretName *string) ([]byte, error) { + response, _, err := ibm.IBMClient.GetSecret( + &sm.GetSecretOptions{ + SecretType: core.StringPtr(sm.GetSecretOptionsSecretTypeArbitraryConst), + ID: secretName, + }) + if err != nil { + return nil, err + } + + secret := response.Resources[0].(*sm.SecretResource) + secretData := secret.SecretData.(map[string]interface{}) + arbitrarySecretPayload := secretData["payload"].(string) + return []byte(arbitrarySecretPayload), nil +} + +func getImportCertSecret(ibm *providerIBM, secretName *string, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + response, _, err := ibm.IBMClient.GetSecret( + &sm.GetSecretOptions{ + SecretType: core.StringPtr(sm.CreateSecretOptionsSecretTypeImportedCertConst), + ID: secretName, + }) + if err != nil { + return nil, err + } + + secret := response.Resources[0].(*sm.SecretResource) + secretData := secret.SecretData.(map[string]interface{}) + + if val, ok := secretData[ref.Property]; ok { + return []byte(val.(string)), nil + } + return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key) +} + +func getIamCredentialsSecret(ibm *providerIBM, secretName *string) ([]byte, error) { + response, _, err := ibm.IBMClient.GetSecret( + &sm.GetSecretOptions{ + SecretType: core.StringPtr(sm.CreateSecretOptionsSecretTypeIamCredentialsConst), + ID: secretName, + }) + if err != nil { + return nil, err + } + + secret := response.Resources[0].(*sm.SecretResource) + secretData := *secret.APIKey + + return []byte(secretData), nil +} + +func getUsernamePasswordSecret(ibm *providerIBM, secretName *string, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + response, _, err := ibm.IBMClient.GetSecret( + &sm.GetSecretOptions{ + SecretType: core.StringPtr(sm.CreateSecretOptionsSecretTypeUsernamePasswordConst), + ID: secretName, + }) + if err != nil { + return nil, err + } + + secret := response.Resources[0].(*sm.SecretResource) + secretData := secret.SecretData.(map[string]interface{}) + + if val, ok := secretData[ref.Property]; ok { + return []byte(val.(string)), nil + } + return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key) +} + func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { if utils.IsNil(ibm.IBMClient) { return nil, fmt.Errorf(errUninitalizedIBMProvider) @@ -214,16 +234,13 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa secretData := secret.SecretData.(map[string]interface{}) arbitrarySecretPayload := secretData["payload"].(string) - kv := make(map[string]string) + kv := make(map[string]interface{}) err = json.Unmarshal([]byte(arbitrarySecretPayload), &kv) if err != nil { return nil, fmt.Errorf(errJSONSecretUnmarshal, err) } - secretMap := make(map[string][]byte) - for k, v := range kv { - secretMap[k] = []byte(v) - } + secretMap := byteArrayMap(kv) return secretMap, nil @@ -240,10 +257,7 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa secret := response.Resources[0].(*sm.SecretResource) secretData := secret.SecretData.(map[string]interface{}) - secretMap := make(map[string][]byte) - for k, v := range secretData { - secretMap[k] = []byte(v.(string)) - } + secretMap := byteArrayMap(secretData) return secretMap, nil @@ -278,10 +292,7 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa secret := response.Resources[0].(*sm.SecretResource) secretData := secret.SecretData.(map[string]interface{}) - secretMap := make(map[string][]byte) - for k, v := range secretData { - secretMap[k] = []byte(v.(string)) - } + secretMap := byteArrayMap(secretData) return secretMap, nil @@ -290,6 +301,14 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa } } +func byteArrayMap(secretData map[string]interface{}) map[string][]byte { + secretMap := make(map[string][]byte) + for k, v := range secretData { + secretMap[k] = []byte(v.(string)) + } + return secretMap +} + func (ibm *providerIBM) Close(ctx context.Context) error { return nil } diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go index 8a0a208aa..08f52e2a0 100644 --- a/pkg/provider/register/register.go +++ b/pkg/provider/register/register.go @@ -15,8 +15,9 @@ limitations under the License. package register // packages imported here are registered to the controller schema. -// nolint:golint +// nolint:revive import ( + _ "github.com/external-secrets/external-secrets/pkg/provider/akeyless" _ "github.com/external-secrets/external-secrets/pkg/provider/alibaba" _ "github.com/external-secrets/external-secrets/pkg/provider/aws" _ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault" diff --git a/pkg/provider/vault/vault.go b/pkg/provider/vault/vault.go index 1a993b71f..6bd2ed2a8 100644 --- a/pkg/provider/vault/vault.go +++ b/pkg/provider/vault/vault.go @@ -224,6 +224,8 @@ func (v *client) readSecret(ctx context.Context, path, version string) (map[stri byteMap[k] = []byte(t) case []byte: byteMap[k] = t + case nil: + byteMap[k] = []byte(nil) default: return nil, errors.New(errSecretFormat) } @@ -316,67 +318,115 @@ func getCertFromConfigMap(v *client) ([]byte, error) { } func (v *client) setAuth(ctx context.Context, client Client, cfg *vault.Config) error { + tokenExists, err := setSecretKeyToken(ctx, v, client) + if tokenExists { + return err + } + + tokenExists, err = setAppRoleToken(ctx, v, client) + if tokenExists { + return err + } + + tokenExists, err = setKubernetesAuthToken(ctx, v, client) + if tokenExists { + return err + } + + tokenExists, err = setLdapAuthToken(ctx, v, client) + if tokenExists { + return err + } + + tokenExists, err = setJwtAuthToken(ctx, v, client) + if tokenExists { + return err + } + + tokenExists, err = setCertAuthToken(ctx, v, client, cfg) + if tokenExists { + return err + } + + return errors.New(errAuthFormat) +} + +func setAppRoleToken(ctx context.Context, v *client, client Client) (bool, error) { tokenRef := v.store.Auth.TokenSecretRef if tokenRef != nil { token, err := v.secretKeyRef(ctx, tokenRef) if err != nil { - return err + return true, err } client.SetToken(token) - return nil + return true, nil } + return false, nil +} +func setSecretKeyToken(ctx context.Context, v *client, client Client) (bool, error) { appRole := v.store.Auth.AppRole if appRole != nil { token, err := v.requestTokenWithAppRoleRef(ctx, client, appRole) if err != nil { - return err + return true, err } client.SetToken(token) - return nil + return true, nil } + return false, nil +} +func setKubernetesAuthToken(ctx context.Context, v *client, client Client) (bool, error) { kubernetesAuth := v.store.Auth.Kubernetes if kubernetesAuth != nil { token, err := v.requestTokenWithKubernetesAuth(ctx, client, kubernetesAuth) if err != nil { - return err + return true, err } client.SetToken(token) - return nil + return true, nil } + return false, nil +} +func setLdapAuthToken(ctx context.Context, v *client, client Client) (bool, error) { ldapAuth := v.store.Auth.Ldap if ldapAuth != nil { token, err := v.requestTokenWithLdapAuth(ctx, client, ldapAuth) if err != nil { - return err + return true, err } client.SetToken(token) - return nil + return true, nil } + return false, nil +} +func setJwtAuthToken(ctx context.Context, v *client, client Client) (bool, error) { jwtAuth := v.store.Auth.Jwt if jwtAuth != nil { token, err := v.requestTokenWithJwtAuth(ctx, client, jwtAuth) if err != nil { - return err + return true, err } client.SetToken(token) - return nil + return true, nil } + return false, nil +} +func setCertAuthToken(ctx context.Context, v *client, client Client, cfg *vault.Config) (bool, error) { certAuth := v.store.Auth.Cert if certAuth != nil { token, err := v.requestTokenWithCertAuth(ctx, client, certAuth, cfg) if err != nil { - return err + return true, err } client.SetToken(token) - return nil + return true, nil } - - return errors.New(errAuthFormat) + return false, nil } func (v *client) secretKeyRefForServiceAccount(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) (string, error) { @@ -493,43 +543,16 @@ func kubeParameters(role, jwt string) map[string]string { } func (v *client) requestTokenWithKubernetesAuth(ctx context.Context, client Client, kubernetesAuth *esv1alpha1.VaultKubernetesAuth) (string, error) { - jwtString := "" - if kubernetesAuth.ServiceAccountRef != nil { - jwt, err := v.secretKeyRefForServiceAccount(ctx, kubernetesAuth.ServiceAccountRef) - if err != nil { - return "", err - } - jwtString = jwt - } else if kubernetesAuth.SecretRef != nil { - tokenRef := kubernetesAuth.SecretRef - if tokenRef.Key == "" { - tokenRef = kubernetesAuth.SecretRef.DeepCopy() - tokenRef.Key = "token" - } - jwt, err := v.secretKeyRef(ctx, tokenRef) - if err != nil { - return "", err - } - jwtString = jwt - } else { - // Kubernetes authentication is specified, but without a referenced - // Kubernetes secret. We check if the file path for in-cluster service account - // exists and attempt to use the token for Vault Kubernetes auth. - if _, err := os.Stat(serviceAccTokenPath); err != nil { - return "", fmt.Errorf(errServiceAccount, err) - } - jwtByte, err := ioutil.ReadFile(serviceAccTokenPath) - if err != nil { - return "", fmt.Errorf(errServiceAccount, err) - } - jwtString = string(jwtByte) + jwtString, err := getJwtString(ctx, v, kubernetesAuth) + if err != nil { + return "", err } parameters := kubeParameters(kubernetesAuth.Role, jwtString) url := strings.Join([]string{"/v1", "auth", kubernetesAuth.Path, "login"}, "/") request := client.NewRequest("POST", url) - err := request.SetJSONBody(parameters) + err = request.SetJSONBody(parameters) if err != nil { return "", fmt.Errorf(errVaultReqParams, err) } @@ -554,6 +577,39 @@ func (v *client) requestTokenWithKubernetesAuth(ctx context.Context, client Clie return token, nil } +func getJwtString(ctx context.Context, v *client, kubernetesAuth *esv1alpha1.VaultKubernetesAuth) (string, error) { + if kubernetesAuth.ServiceAccountRef != nil { + jwt, err := v.secretKeyRefForServiceAccount(ctx, kubernetesAuth.ServiceAccountRef) + if err != nil { + return "", err + } + return jwt, nil + } else if kubernetesAuth.SecretRef != nil { + tokenRef := kubernetesAuth.SecretRef + if tokenRef.Key == "" { + tokenRef = kubernetesAuth.SecretRef.DeepCopy() + tokenRef.Key = "token" + } + jwt, err := v.secretKeyRef(ctx, tokenRef) + if err != nil { + return "", err + } + return jwt, nil + } else { + // Kubernetes authentication is specified, but without a referenced + // Kubernetes secret. We check if the file path for in-cluster service account + // exists and attempt to use the token for Vault Kubernetes auth. + if _, err := os.Stat(serviceAccTokenPath); err != nil { + return "", fmt.Errorf(errServiceAccount, err) + } + jwtByte, err := ioutil.ReadFile(serviceAccTokenPath) + if err != nil { + return "", fmt.Errorf(errServiceAccount, err) + } + return string(jwtByte), nil + } +} + func (v *client) requestTokenWithLdapAuth(ctx context.Context, client Client, ldapAuth *esv1alpha1.VaultLdapAuth) (string, error) { username := strings.TrimSpace(ldapAuth.Username) diff --git a/pkg/provider/vault/vault_test.go b/pkg/provider/vault/vault_test.go index 7b841e358..6bb2d8003 100644 --- a/pkg/provider/vault/vault_test.go +++ b/pkg/provider/vault/vault_test.go @@ -41,7 +41,7 @@ const ( secretDataString = "some-creds" ) -func makeValidSecretStore() *esv1alpha1.SecretStore { +func makeValidSecretStoreWithVersion(v esv1alpha1.VaultKVStoreVersion) *esv1alpha1.SecretStore { return &esv1alpha1.SecretStore{ ObjectMeta: metav1.ObjectMeta{ Name: "vault-store", @@ -52,7 +52,7 @@ func makeValidSecretStore() *esv1alpha1.SecretStore { Vault: &esv1alpha1.VaultProvider{ Server: "vault.example.com", Path: "secret", - Version: esv1alpha1.VaultKVStoreV2, + Version: v, Auth: esv1alpha1.VaultAuth{ Kubernetes: &esv1alpha1.VaultKubernetesAuth{ Path: "kubernetes", @@ -68,6 +68,10 @@ func makeValidSecretStore() *esv1alpha1.SecretStore { } } +func makeValidSecretStore() *esv1alpha1.SecretStore { + return makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2) +} + func makeValidSecretStoreWithCerts() *esv1alpha1.SecretStore { return &esv1alpha1.SecretStore{ ObjectMeta: metav1.ObjectMeta{ @@ -136,14 +140,35 @@ func newVaultResponse(data *vault.Secret) *vault.Response { } } -func newVaultTokenIDResponse(token string) *vault.Response { +func newVaultResponseWithData(data map[string]interface{}) *vault.Response { return newVaultResponse(&vault.Secret{ - Data: map[string]interface{}{ - "id": token, - }, + Data: data, }) } +func newVaultTokenIDResponse(token string) *vault.Response { + return newVaultResponseWithData(map[string]interface{}{ + "id": token, + }) +} + +type args struct { + newClientFunc func(c *vault.Config) (Client, error) + store esv1alpha1.GenericStore + kube kclient.Client + ns string +} + +type want struct { + err error +} + +type testCase struct { + reason string + args args + want want +} + func clientWithLoginMock(c *vault.Config) (Client, error) { return &fake.VaultClient{ MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}), @@ -182,22 +207,7 @@ MIICsTCCAZkCFEJJ4daz5sxkFlzq9n1djLEuG7bmMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMMCHZh `) secretData := []byte(secretDataString) - type args struct { - newClientFunc func(c *vault.Config) (Client, error) - store esv1alpha1.GenericStore - kube kclient.Client - ns string - } - - type want struct { - err error - } - - cases := map[string]struct { - reason string - args args - want want - }{ + cases := map[string]testCase{ "InvalidVaultStore": { reason: "Should return error if given an invalid vault store.", args: args{ @@ -472,22 +482,35 @@ MIICsTCCAZkCFEJJ4daz5sxkFlzq9n1djLEuG7bmMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMMCHZh for name, tc := range cases { t.Run(name, func(t *testing.T) { - conn := &connector{ - newVaultClient: tc.args.newClientFunc, - } - if tc.args.newClientFunc == nil { - conn.newVaultClient = newVaultClient - } - _, err := conn.NewClient(context.Background(), tc.args.store, tc.args.kube, tc.args.ns) - if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { - t.Errorf("\n%s\nvault.New(...): -want error, +got error:\n%s", tc.reason, diff) - } + vaultTest(t, name, tc) }) } } +func vaultTest(t *testing.T, name string, tc testCase) { + conn := &connector{ + newVaultClient: tc.args.newClientFunc, + } + if tc.args.newClientFunc == nil { + conn.newVaultClient = newVaultClient + } + _, err := conn.NewClient(context.Background(), tc.args.store, tc.args.kube, tc.args.ns) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nvault.New(...): -want error, +got error:\n%s", tc.reason, diff) + } +} + func TestGetSecretMap(t *testing.T) { errBoom := errors.New("boom") + secret := map[string]interface{}{ + "access_key": "access_key", + "access_secret": "access_secret", + } + secretWithNilVal := map[string]interface{}{ + "access_key": "access_key", + "access_secret": "access_secret", + "token": nil, + } type args struct { store *esv1alpha1.VaultProvider @@ -506,6 +529,74 @@ func TestGetSecretMap(t *testing.T) { args args want want }{ + "ReadSecretKV1": { + reason: "Should map the secret even if it has a nil value", + args: args{ + store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV1).Spec.Provider.Vault, + vClient: &fake.VaultClient{ + MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}), + MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn( + newVaultResponseWithData(secret), nil, + ), + }, + }, + want: want{ + err: nil, + }, + }, + "ReadSecretKV2": { + reason: "Should map the secret even if it has a nil value", + args: args{ + store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2).Spec.Provider.Vault, + vClient: &fake.VaultClient{ + MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}), + MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn( + newVaultResponseWithData( + map[string]interface{}{ + "data": secret, + }, + ), nil, + ), + }, + }, + want: want{ + err: nil, + }, + }, + "ReadSecretWithNilValueKV1": { + reason: "Should map the secret even if it has a nil value", + args: args{ + store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV1).Spec.Provider.Vault, + vClient: &fake.VaultClient{ + MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}), + MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn( + newVaultResponseWithData(secretWithNilVal), nil, + ), + }, + }, + want: want{ + err: nil, + }, + }, + "ReadSecretWithNilValueKV2": { + reason: "Should map the secret even if it has a nil value", + args: args{ + store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2).Spec.Provider.Vault, + vClient: &fake.VaultClient{ + MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}), + MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn( + newVaultResponseWithData( + map[string]interface{}{ + "data": secretWithNilVal, + }, + ), nil, + ), + }, + }, + want: want{ + err: nil, + }, + }, "ReadSecretError": { reason: "Should return error if vault client fails to read secret.", args: args{ diff --git a/pkg/provider/yandex/lockbox/lockbox_test.go b/pkg/provider/yandex/lockbox/lockbox_test.go index d2ac2b602..83eee3913 100644 --- a/pkg/provider/yandex/lockbox/lockbox_test.go +++ b/pkg/provider/yandex/lockbox/lockbox_test.go @@ -35,6 +35,12 @@ import ( "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake" ) +const ( + errMissingKey = "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name" + errSecretPayloadPermissionDenied = "unable to request secret payload to get secret: permission denied" + errSecretPayloadNotFound = "unable to request secret payload to get secret: secret not found" +) + func TestNewClient(t *testing.T) { ctx := context.Background() const namespace = "namespace" @@ -54,17 +60,17 @@ func TestNewClient(t *testing.T) { k8sClient := clientfake.NewClientBuilder().Build() secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace) - tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name") + tassert.EqualError(t, err, errMissingKey) tassert.Nil(t, secretClient) store.Spec.Provider.YandexLockbox.Auth = esv1alpha1.YandexLockboxAuth{} secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace) - tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name") + tassert.EqualError(t, err, errMissingKey) tassert.Nil(t, secretClient) store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey = esmeta.SecretKeySelector{} secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace) - tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name") + tassert.EqualError(t, err, errMissingKey) tassert.Nil(t, secretClient) const authorizedKeySecretName = "authorizedKeySecretName" @@ -248,7 +254,7 @@ func TestGetSecretUnauthorized(t *testing.T) { secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace) tassert.Nil(t, err) _, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID}) - tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied") + tassert.EqualError(t, err, errSecretPayloadPermissionDenied) } func TestGetSecretNotFound(t *testing.T) { @@ -271,7 +277,7 @@ func TestGetSecretNotFound(t *testing.T) { secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace) tassert.Nil(t, err) _, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"}) - tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found") + tassert.EqualError(t, err, errSecretPayloadNotFound) secretID, _ := lockboxBackend.CreateSecret(authorizedKey, textEntry("k1", "v1"), @@ -320,11 +326,11 @@ func TestGetSecretWithTwoNamespaces(t *testing.T) { tassert.Nil(t, err) data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2}) tassert.Nil(t, data) - tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied") + tassert.EqualError(t, err, errSecretPayloadPermissionDenied) data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1}) tassert.Nil(t, data) - tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied") + tassert.EqualError(t, err, errSecretPayloadPermissionDenied) data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2}) tassert.Equal(t, v2, string(data)) tassert.Nil(t, err) @@ -381,11 +387,11 @@ func TestGetSecretWithTwoApiEndpoints(t *testing.T) { tassert.Nil(t, err) data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2}) tassert.Nil(t, data) - tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found") + tassert.EqualError(t, err, errSecretPayloadNotFound) data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1}) tassert.Nil(t, data) - tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found") + tassert.EqualError(t, err, errSecretPayloadNotFound) data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2}) tassert.Equal(t, v2, string(data)) tassert.Nil(t, err) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index d0939dc45..70fa1948c 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -39,7 +39,14 @@ func MergeStringMap(dest, src map[string]string) { // IsNil checks if an Interface is nil. func IsNil(i interface{}) bool { - return i == nil || reflect.ValueOf(i).IsNil() + if i == nil { + return true + } + value := reflect.ValueOf(i) + if value.Type().Kind() == reflect.Ptr { + return value.IsNil() + } + return false } // ObjectHash calculates md5 sum of the data contained in the secret. diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index c830c1a8d..fae33ad2f 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -17,6 +17,7 @@ package utils import ( "testing" + vault "github.com/oracle/oci-go-sdk/v45/vault" v1 "k8s.io/api/core/v1" ) @@ -60,3 +61,88 @@ func TestObjectHash(t *testing.T) { }) } } + +func TestIsNil(t *testing.T) { + tbl := []struct { + name string + val interface{} + exp bool + }{ + { + name: "simple nil val", + val: nil, + exp: true, + }, + { + name: "nil slice", + val: (*[]struct{})(nil), + exp: true, + }, + { + name: "struct pointer", + val: &testing.T{}, + exp: false, + }, + { + name: "struct", + val: testing.T{}, + exp: false, + }, + { + name: "slice of struct", + val: []struct{}{{}}, + exp: false, + }, + { + name: "slice of ptr", + val: []*testing.T{nil}, + exp: false, + }, + { + name: "slice", + val: []struct{}(nil), + exp: false, + }, + { + name: "int default value", + val: 0, + exp: false, + }, + { + name: "empty str", + val: "", + exp: false, + }, + { + name: "oracle vault", + val: vault.VaultsClient{}, + exp: false, + }, + { + name: "func", + val: func() { + // noop for testing and to make linter happy + }, + exp: false, + }, + { + name: "channel", + val: make(chan struct{}), + exp: false, + }, + { + name: "map", + val: map[string]string{}, + exp: false, + }, + } + + for _, row := range tbl { + t.Run(row.name, func(t *testing.T) { + res := IsNil(row.val) + if res != row.exp { + t.Errorf("IsNil(%#v)=%t, expected %t", row.val, res, row.exp) + } + }) + } +} diff --git a/tools.go b/tools.go index 16d3fad6c..56fa2007e 100644 --- a/tools.go +++ b/tools.go @@ -4,5 +4,6 @@ package tools import ( + _ "github.com/onsi/ginkgo/ginkgo" _ "sigs.k8s.io/controller-tools/cmd/controller-gen" )