1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Update with main branch

This commit is contained in:
Sebastian Gomez 2022-01-17 16:33:40 -05:00
commit 18a9bb1745
120 changed files with 6468 additions and 939 deletions

1
.github/PAUL.yaml vendored
View file

@ -13,6 +13,7 @@ maintainers:
- Flydiverny
- gabibeyer
- ricardoptcosta
- rodrmartinez
# Allows for the /label and /remove-label commands
# usage: /label enhancement
# usage: /remove-label enhancement

View file

@ -10,7 +10,7 @@ on:
env:
# Common versions
GO_VERSION: '1.16'
GO_VERSION: '1.17'
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
@ -21,7 +21,7 @@ env:
# a step 'if env.GHCR_USERNAME' != ""', so we copy these to succinctly test whether
# credentials have been provided before trying to run steps that need them.
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
# Sonar
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@ -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,14 +107,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-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') }}
@ -151,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') }}
@ -171,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}}
@ -218,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') }}

View file

@ -5,6 +5,10 @@ on:
branches:
- main
env:
# Common versions
GO_VERSION: '1.17'
jobs:
deploy:
runs-on: ubuntu-18.04

181
.github/workflows/e2e-managed.yml vendored Normal file
View file

@ -0,0 +1,181 @@
# Run secret-dependent e2e tests only after /ok-to-test-managed approval
on:
pull_request:
repository_dispatch:
types: [ok-to-test-managed-command]
env:
# Common versions
GO_VERSION: '1.17'
GOLANGCI_VERSION: 'v1.33'
DOCKER_BUILDX_VERSION: 'v0.4.2'
# Common users. We can't run a step 'if secrets.GHCR_USERNAME != ""' but we can run
# a step 'if env.GHCR_USERNAME' != ""', so we copy these to succinctly test whether
# credentials have been provided before trying to run steps that need them.
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
GCP_SM_SA_JSON: ${{ secrets.GCP_SM_SA_JSON}}
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID}}
TF_VAR_GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID}}
GCP_SM_SA_GKE_JSON: ${{ secrets.GCP_SM_SA_GKE_JSON}}
GCP_GKE_CLUSTER: test-cluster
GCP_GKE_ZONE: ${{ secrets.GCP_GKE_ZONE}}
GCP_GSA_NAME: ${{ secrets.GCP_GSA_NAME}} # Goolge Service Account
GCP_KSA_NAME: ${{ secrets.GCP_KSA_NAME}} # Kubernetes Service Account
TF_VAR_GCP_GSA_NAME: ${{ secrets.GCP_GSA_NAME}} # Goolge Service Account for tf
TF_VAR_GCP_KSA_NAME: ${{ secrets.GCP_KSA_NAME}} # Kubernetes Service Account for tf
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID}}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET}}
TENANT_ID: ${{ secrets.TENANT_ID}}
VAULT_URL: ${{ secrets.VAULT_URL}}
IMAGE_REGISTRY: ghcr.io/external-secrets/external-secrets
E2E_IMAGE_REGISTRY: ghcr.io/external-secrets/external-secrets-e2e
E2E_VERSION: test
name: e2e tests
jobs:
# Repo owner has commented /ok-to-test-managed on a (fork-based) pull request
integration-fork-managed:
runs-on: ubuntu-latest
if:
github.event_name == 'repository_dispatch'
steps:
# Check out merge commit
- name: Fork based /ok-to-test-managed checkout
uses: actions/checkout@v2
with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
- name: Fetch History
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- name: Find the Go Cache
id: go
run: |
echo "::set-output name=build-cache::$(go env GOCACHE)"
echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
- name: Cache the Go Build Cache
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.7
with:
path: ${{ steps.go.outputs.mod-cache }}
key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-pkg-
- name: Setup gcloud CLI
uses: google-github-actions/setup-gcloud@master
with:
service_account_key: ${{ env.GCP_SM_SA_GKE_JSON }}
project_id: ${{ env.GCP_PROJECT_ID }}
- name: Setup TFLint
uses: terraform-linters/setup-tflint@v1
with:
tflint_version: v0.28.0 # Must be specified. See: https://github.com/terraform-linters/tflint/releases for latest versions
- name: Run TFLint
run: find ${{ github.workspace }} | grep tf$ | xargs -n1 dirname | xargs -IXXX -n1 /bin/sh -c 'set -o errexit; cd XXX; pwd; tflint --loglevel=info .; cd - >/dev/null'
- name: Setup TF Gcloud Provider
run: |-
mkdir -p terraform/gcp/secrets
echo ${GCP_SM_SA_GKE_JSON} > terraform/gcp/secrets/gcloud-service-account-key.json
- name: Show TF GKE
run: |-
make tf.show.gcp
- name: Setup Infracost
uses: infracost/actions/setup@v1
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate Infracost JSON for GKE
run: infracost breakdown --path terraform/gcp/plan.json --format json --out-file /tmp/infracost.json
- name: Post Infracost comment
uses: infracost/actions/comment@v1
with:
path: /tmp/infracost.json
# Choose the commenting behavior, 'update' is a good default:
behavior: update # Create a single comment and update it. The "quietest" option.
# behavior: delete-and-new # Delete previous comments and create a new one.
# behavior: hide-and-new # Minimize previous comments and create a new one.
# behavior: new # Create a new cost estimate comment on every push.
- name: Apply TF GKE
run: |-
make tf.apply.gcp
- name: Get the GKE credentials
run: |-
gcloud container clusters get-credentials "$GCP_GKE_CLUSTER" --zone "$GCP_GKE_ZONE" --project "$GCP_PROJECT_ID"
- name: Login to Docker
uses: docker/login-action@v1
if: env.GHCR_USERNAME != ''
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Run e2e Tests for GCP
run: |
export E2E_VERSION=$GITHUB_SHA
export PR_IMG_TAG=$GITHUB_SHA
export PATH=$PATH:$(go env GOPATH)/bin
go get github.com/onsi/ginkgo/ginkgo
make test.e2e.managed FOCUS="gcpmanaged"
- name: Destroy TF GKE
if: always()
run: |-
make tf.destroy.gcp
# Update check run called "integration-fork"
- uses: actions/github-script@v1
id: update-check-run
if: ${{ always() }}
env:
number: ${{ github.event.client_payload.pull_request.number }}
job: ${{ github.job }}
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
conclusion: ${{ job.status }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: pull } = await github.pulls.get({
...context.repo,
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;

View file

@ -6,7 +6,7 @@ on:
env:
# Common versions
GO_VERSION: '1.16'
GO_VERSION: '1.17'
GOLANGCI_VERSION: 'v1.33'
DOCKER_BUILDX_VERSION: 'v0.4.2'
@ -15,11 +15,16 @@ env:
# credentials have been provided before trying to run steps that need them.
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
GCP_SM_SA_JSON: ${{ secrets.GCP_SM_SA_JSON}}
GCP_GKE_ZONE: ${{ secrets.GCP_GKE_ZONE}}
GCP_GSA_NAME: ${{ secrets.GCP_GSA_NAME}} # Goolge Service Account
GCP_KSA_NAME: ${{ secrets.GCP_KSA_NAME}} # Kubernetes Service Account
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID}}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID}}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET}}
TENANT_ID: ${{ secrets.TENANT_ID}}
VAULT_URL: ${{ secrets.VAULT_URL}}
E2E_IMAGE_REGISTRY: local/external-secrets-e2e
E2E_VERSION: test
name: e2e tests
@ -50,14 +55,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 +118,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') }}

View file

@ -35,7 +35,7 @@ jobs:
python-version: 3.7
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.1.0
uses: helm/chart-testing-action@v2.2.0
- name: Run chart-testing (list-changed)
id: list-changed

View file

@ -0,0 +1,35 @@
# If someone with write access comments "/ok-to-test-managed" on a pull request, emit a repository_dispatch event
name: Ok To Test
on:
issue_comment:
types: [created]
jobs:
ok-to-test-managed:
runs-on: ubuntu-latest
# Only run for PRs, not issue comments
if: ${{ github.event.issue.pull_request }}
steps:
# Generate a GitHub App installation access token from an App ID and private key
# To create a new GitHub App:
# https://developer.github.com/apps/building-github-apps/creating-a-github-app/
# See app.yml for an example app manifest
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v2.3.0
env:
TOKEN: ${{ steps.generate_token.outputs.token }}
with:
token: ${{ env.TOKEN }} # GitHub App installation access token
# token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
reaction-token: ${{ secrets.GITHUB_TOKEN }}
issue-type: pull-request
commands: ok-to-test-managed
permission: maintain

41
.gitignore vendored
View file

@ -1,3 +1,5 @@
.DS_Store
/bin
/vendor
cover.out
@ -18,3 +20,42 @@ deploy/charts/external-secrets/templates/crds/*.yaml
site/
e2e/k8s/deploy
e2e/e2e.test
# tf ignores
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
#
*.tfvars
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
**/secrets/**
.terraform.lock.hcl

View file

@ -63,7 +63,6 @@ linters:
- gosimple
- govet
- ineffassign
- interfacer
- lll
- misspell
- nakedret

10
ADOPTERS.md Normal file
View file

@ -0,0 +1,10 @@
# External Secrets Operator Adopters
<!-- Add yourself here if you are using ESO in your company or your project! -->
- [Pento](https://www.pento.io/)
- [Mixpanel](https://mixpanel.com)
- [K8S Website Infra](https://k8s.io/)
Countless others that can't disclose that information! :)

View file

@ -1,4 +1,4 @@
FROM alpine:3.14.3
FROM gcr.io/distroless/static
ARG TARGETOS
ARG TARGETARCH
COPY bin/external-secrets-${TARGETOS}-${TARGETARCH} /bin/external-secrets

View file

@ -16,11 +16,14 @@ all: $(addprefix build-,$(ARCH))
# Image registry for build/push image targets
IMAGE_REGISTRY ?= ghcr.io/external-secrets/external-secrets
PR_IMG_TAG ?=
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true"
CRD_DIR ?= deploy/crds
HELM_DIR ?= deploy/charts/external-secrets
TF_DIR ?= terraform
OUTPUT_DIR ?= bin
@ -88,6 +91,12 @@ test.e2e: generate ## Run e2e tests
$(MAKE) -C ./e2e test
@$(OK) go test unit-tests
.PHONY: test.e2e.managed
test.e2e.managed: generate ## Run e2e tests
@$(INFO) go test e2e-tests
$(MAKE) -C ./e2e test.managed
@$(OK) go test unit-tests
.PHONY: build
build: $(addprefix build-,$(ARCH)) ## Build binary
@ -220,6 +229,32 @@ docker.promote: ## Promote the docker image to the registry
docker manifest push $(IMAGE_REGISTRY):$(RELEASE_TAG)
@$(OK) docker push $(RELEASE_TAG) \
# ====================================================================================
# Terraform
tf.plan.gcp: ## Runs terrform plan for gcp provider bringing GKE up
@cd $(TF_DIR)/gcp; \
terraform init; \
terraform plan -auto-approve
tf.apply.gcp: ## Runs terrform apply for gcp provider bringing GKE up
@cd $(TF_DIR)/gcp; \
terraform init; \
terraform apply -auto-approve
tf.destroy.gcp: ## Runs terrform destroy for gcp provider bringing GKE down
@cd $(TF_DIR)/gcp; \
terraform init; \
terraform destroy -auto-approve
tf.show.gcp: ## Runs terrform show for gcp and outputs to a file
@cd $(TF_DIR)/gcp; \
terraform init; \
terraform plan -out tfplan.binary; \
terraform show -json tfplan.binary > plan.json
# ====================================================================================
# Help

View file

@ -21,7 +21,8 @@ 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)
- [Generic Webhook](https://external-secrets.io/provider-webhook)
## Stability and Support Level
@ -29,10 +30,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) |
| [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/) | alpha | [ESO Org](https://github.com/external-secrets) |
| [GCP SM](https://external-secrets.io/provider-google-secrets-manager/) | stable | [ESO Org](https://github.com/external-secrets) |
### Community maintained:
@ -43,9 +44,9 @@ Multiple people and organizations are joining efforts to create a single Externa
| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) | alpha | [@AndreyZamyslov](https://github.com/AndreyZamyslov) [@knelasevero](https://github.com/knelasevero) |
| [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) |
| [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) | alpha | [@KianTigger](https://github.com/KianTigger) [@EladGabay](https://github.com/EladGabay) |
| [Akeyless]( https://external-secrets.io/provider-akeyless) | alpha | [@renanaAkeyless](https://github.com/renanaAkeyless) |
| [Generic Webhook](https://external-secrets.io/provider-webhook) | alpha | [@willemm](https://github.com/willemm) |
## Documentation
@ -65,6 +66,10 @@ We welcome and encourage contributions to this project! Please read the [Develop
Please report vulnerabilities by email to contact@external-secrets.io, also see our [security policy](SECURITY.md) for details.
## Adopters
Please create a PR and add your company or your project to our [ADOPTERS](ADOPTERS.md) file if you are using our project!
## Kicked off by
![](assets/CS_logo_1.png)

View file

@ -8,8 +8,9 @@ The external-secrets project is released on a as-needed basis. Feel free to open
1. Run `Create Release` Action to create a new release, pass in the desired version number to release.
2. GitHub Release, Changelog will be created by the `release.yml` workflow which also promotes the container image.
3. (optional) update Helm Chart
4. Announce the new release in the `#external-secrets` Kubernetes Slack
3. update Helm Chart, see below
4. update OLM bundle, see [helm-operator docs](https://github.com/external-secrets/external-secrets-helm-operator/blob/main/docs/release.md#operatorhubio)
5. Announce the new release in the `#external-secrets` Kubernetes Slack
## Release Helm Chart

View file

@ -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.

View file

@ -19,7 +19,10 @@ import (
)
type GCPSMAuth struct {
SecretRef GCPSMAuthSecretRef `json:"secretRef"`
// +optional
SecretRef *GCPSMAuthSecretRef `json:"secretRef,omitempty"`
// +optional
WorkloadIdentity *GCPWorkloadIdentity `json:"workloadIdentity,omitempty"`
}
type GCPSMAuthSecretRef struct {
@ -28,6 +31,12 @@ type GCPSMAuthSecretRef struct {
SecretAccessKey esmeta.SecretKeySelector `json:"secretAccessKeySecretRef,omitempty"`
}
type GCPWorkloadIdentity struct {
ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
ClusterLocation string `json:"clusterLocation"`
ClusterName string `json:"clusterName"`
}
// GCPSMProvider Configures a store to sync secrets using the GCP Secret Manager provider.
type GCPSMProvider struct {
// Auth defines the information necessary to authenticate against GCP

View file

@ -25,11 +25,14 @@ type OracleProvider struct {
// User is an access OCID specific to the account.
User string `json:"user,omitempty"`
// projectID is an access token specific to the secret.
// Tenancy is the tenancy OCID where secret is located.
Tenancy string `json:"tenancy,omitempty"`
// projectID is an access token specific to the secret.
// Region is the region where secret is located.
Region string `json:"region,omitempty"`
// Vault is the vault's OCID of the specific vault where secret is located.
Vault string `json:"vault,omitempty"`
}
type OracleAuth struct {
@ -38,9 +41,9 @@ type OracleAuth struct {
}
type OracleSecretRef struct {
// The Access Token is used for authentication
// PrivateKey is the user's API Signing Key in PEM format, used for authentication.
PrivateKey esmeta.SecretKeySelector `json:"privatekey,omitempty"`
// projectID is an access token specific to the secret.
// Fingerprint is the fingerprint of the API private key.
Fingerprint esmeta.SecretKeySelector `json:"fingerprint,omitempty"`
}

View file

@ -28,6 +28,10 @@ type SecretStoreSpec struct {
// Used to configure the provider. Only one provider may be set
Provider *SecretStoreProvider `json:"provider"`
// Used to configure http retries if failed
// +optional
RetrySettings *SecretStoreRetrySettings `json:"retrySettings,omitempty"`
}
// SecretStoreProvider contains the provider-specific configration.
@ -73,6 +77,15 @@ type SecretStoreProvider struct {
// Alibaba configures this store to sync secrets using Alibaba Cloud provider
// +optional
Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
// Webhook configures this store to sync secrets using a generic templated webhook
// +optional
Webhook *WebhookProvider `json:"webhook,omitempty"`
}
type SecretStoreRetrySettings struct {
MaxRetries *int32 `json:"maxRetries,omitempty"`
RetryInterval *string `json:"retryInterval,omitempty"`
}
type SecretStoreConditionType string

View file

@ -46,8 +46,8 @@ type CAProvider struct {
Key string `json:"key,omitempty"`
// The namespace the Provider type is in.
// +kubebuilder:default:="Default"
Namespace string `json:"namespace"`
// +optional
Namespace *string `json:"namespace,omitempty"`
}
// Configures an store to sync secrets using a HashiCorp Vault
@ -63,7 +63,8 @@ type VaultProvider struct {
// "secret". The v2 KV secret engine version specific "/data" path suffix
// for fetching secrets from Vault is optional and will be appended
// if not present in specified path.
Path string `json:"path"`
// +optional
Path *string `json:"path"`
// Version is the Vault KV secret engine version. This can be either "v1" or
// "v2". Version defaults to "v2".
@ -173,6 +174,11 @@ type VaultKubernetesAuth struct {
// VaultLdapAuth authenticates with Vault using the LDAP authentication method,
// with the username and password stored in a Kubernetes Secret resource.
type VaultLdapAuth struct {
// Path where the LDAP authentication backend is mounted
// in Vault, e.g: "ldap"
// +kubebuilder:default=ldap
Path string `json:"path"`
// Username is a LDAP user name used to authenticate using the LDAP Vault
// authentication method
Username string `json:"username"`
@ -186,6 +192,11 @@ type VaultLdapAuth struct {
// VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication
// method, with the role name and token stored in a Kubernetes Secret resource.
type VaultJwtAuth struct {
// Path where the JWT authentication backend is mounted
// in Vault, e.g: "jwt"
// +kubebuilder:default=jwt
Path string `json:"path"`
// Role is a JWT role to authenticate using the JWT/OIDC Vault
// authentication method
// +optional

View file

@ -0,0 +1,101 @@
/*
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 (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
// AkeylessProvider Configures an store to sync secrets using Akeyless KV.
type WebhookProvider struct {
// Webhook Method
// +optional, default GET
Method string `json:"method,omitempty"`
// Webhook url to call
URL string `json:"url"`
// Headers
// +optional
Headers map[string]string `json:"headers,omitempty"`
// Body
// +optional
Body string `json:"body,omitempty"`
// Timeout
// +optional
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Result formatting
Result WebhookResult `json:"result"`
// Secrets to fill in templates
// These secrets will be passed to the templating function as key value pairs under the given name
// +optional
Secrets []WebhookSecret `json:"secrets,omitempty"`
// PEM encoded CA bundle used to validate webhook server certificate. Only used
// if the Server URL is using HTTPS protocol. This parameter is ignored for
// plain HTTP protocol connection. If not set the system root certificates
// are used to validate the TLS connection.
// +optional
CABundle []byte `json:"caBundle,omitempty"`
// The provider for the CA bundle to use to validate webhook server certificate.
// +optional
CAProvider *WebhookCAProvider `json:"caProvider,omitempty"`
}
type WebhookCAProviderType string
const (
WebhookCAProviderTypeSecret WebhookCAProviderType = "Secret"
WebhookCAProviderTypeConfigMap WebhookCAProviderType = "ConfigMap"
)
// Defines a location to fetch the cert for the webhook provider from.
type WebhookCAProvider struct {
// The type of provider to use such as "Secret", or "ConfigMap".
// +kubebuilder:validation:Enum="Secret";"ConfigMap"
Type WebhookCAProviderType `json:"type"`
// The name of the object located at the provider type.
Name string `json:"name"`
// The key the value inside of the provider type to use, only used with "Secret" type
// +kubebuilder:validation:Optional
Key string `json:"key,omitempty"`
// The namespace the Provider type is in.
// +optional
Namespace *string `json:"namespace,omitempty"`
}
type WebhookResult struct {
// Json path of return value
// +optional
JSONPath string `json:"jsonPath,omitempty"`
}
type WebhookSecret struct {
// Name of this secret in templates
Name string `json:"name"`
// Secret ref to fill in credentials
SecretRef esmeta.SecretKeySelector `json:"secretRef"`
}

View file

@ -24,6 +24,10 @@ type YandexLockboxAuth struct {
AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
}
type YandexLockboxCAProvider struct {
Certificate esmeta.SecretKeySelector `json:"certSecretRef,omitempty"`
}
// YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.
type YandexLockboxProvider struct {
// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
@ -32,4 +36,8 @@ type YandexLockboxProvider struct {
// Auth defines the information necessary to authenticate against Yandex Lockbox
Auth YandexLockboxAuth `json:"auth"`
// The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
// +optional
CAProvider *YandexLockboxCAProvider `json:"caProvider,omitempty"`
}

View file

@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
@ -242,6 +243,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)
@ -257,6 +263,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.
@ -272,6 +283,11 @@ func (in *AzureKVProvider) DeepCopy() *AzureKVProvider {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CAProvider) DeepCopyInto(out *CAProvider) {
*out = *in
if in.Namespace != nil {
in, out := &in.Namespace, &out.Namespace
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAProvider.
@ -586,7 +602,16 @@ func (in *ExternalSecretTemplateMetadata) DeepCopy() *ExternalSecretTemplateMeta
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
*out = *in
in.SecretRef.DeepCopyInto(&out.SecretRef)
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(GCPSMAuthSecretRef)
(*in).DeepCopyInto(*out)
}
if in.WorkloadIdentity != nil {
in, out := &in.WorkloadIdentity, &out.WorkloadIdentity
*out = new(GCPWorkloadIdentity)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPSMAuth.
@ -631,6 +656,22 @@ func (in *GCPSMProvider) DeepCopy() *GCPSMProvider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GCPWorkloadIdentity) DeepCopyInto(out *GCPWorkloadIdentity) {
*out = *in
in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPWorkloadIdentity.
func (in *GCPWorkloadIdentity) DeepCopy() *GCPWorkloadIdentity {
if in == nil {
return nil
}
out := new(GCPWorkloadIdentity)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
*out = *in
@ -893,6 +934,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(AlibabaProvider)
(*in).DeepCopyInto(*out)
}
if in.Webhook != nil {
in, out := &in.Webhook, &out.Webhook
*out = new(WebhookProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@ -920,6 +966,31 @@ func (in *SecretStoreRef) DeepCopy() *SecretStoreRef {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretStoreRetrySettings) DeepCopyInto(out *SecretStoreRetrySettings) {
*out = *in
if in.MaxRetries != nil {
in, out := &in.MaxRetries, &out.MaxRetries
*out = new(int32)
**out = **in
}
if in.RetryInterval != nil {
in, out := &in.RetryInterval, &out.RetryInterval
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreRetrySettings.
func (in *SecretStoreRetrySettings) DeepCopy() *SecretStoreRetrySettings {
if in == nil {
return nil
}
out := new(SecretStoreRetrySettings)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) {
*out = *in
@ -928,6 +999,11 @@ func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) {
*out = new(SecretStoreProvider)
(*in).DeepCopyInto(*out)
}
if in.RetrySettings != nil {
in, out := &in.RetrySettings, &out.RetrySettings
*out = new(SecretStoreRetrySettings)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreSpec.
@ -1177,6 +1253,11 @@ func (in *VaultLdapAuth) DeepCopy() *VaultLdapAuth {
func (in *VaultProvider) DeepCopyInto(out *VaultProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
if in.Path != nil {
in, out := &in.Path, &out.Path
*out = new(string)
**out = **in
}
if in.Namespace != nil {
in, out := &in.Namespace, &out.Namespace
*out = new(string)
@ -1190,7 +1271,7 @@ func (in *VaultProvider) DeepCopyInto(out *VaultProvider) {
if in.CAProvider != nil {
in, out := &in.CAProvider, &out.CAProvider
*out = new(CAProvider)
**out = **in
(*in).DeepCopyInto(*out)
}
}
@ -1204,6 +1285,102 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookCAProvider) DeepCopyInto(out *WebhookCAProvider) {
*out = *in
if in.Namespace != nil {
in, out := &in.Namespace, &out.Namespace
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookCAProvider.
func (in *WebhookCAProvider) DeepCopy() *WebhookCAProvider {
if in == nil {
return nil
}
out := new(WebhookCAProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookProvider) DeepCopyInto(out *WebhookProvider) {
*out = *in
if in.Headers != nil {
in, out := &in.Headers, &out.Headers
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout
*out = new(v1.Duration)
**out = **in
}
out.Result = in.Result
if in.Secrets != nil {
in, out := &in.Secrets, &out.Secrets
*out = make([]WebhookSecret, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.CABundle != nil {
in, out := &in.CABundle, &out.CABundle
*out = make([]byte, len(*in))
copy(*out, *in)
}
if in.CAProvider != nil {
in, out := &in.CAProvider, &out.CAProvider
*out = new(WebhookCAProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookProvider.
func (in *WebhookProvider) DeepCopy() *WebhookProvider {
if in == nil {
return nil
}
out := new(WebhookProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookResult) DeepCopyInto(out *WebhookResult) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookResult.
func (in *WebhookResult) DeepCopy() *WebhookResult {
if in == nil {
return nil
}
out := new(WebhookResult)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookSecret) DeepCopyInto(out *WebhookSecret) {
*out = *in
in.SecretRef.DeepCopyInto(&out.SecretRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSecret.
func (in *WebhookSecret) DeepCopy() *WebhookSecret {
if in == nil {
return nil
}
out := new(WebhookSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
*out = *in
@ -1220,10 +1397,31 @@ func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YandexLockboxCAProvider) DeepCopyInto(out *YandexLockboxCAProvider) {
*out = *in
in.Certificate.DeepCopyInto(&out.Certificate)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxCAProvider.
func (in *YandexLockboxCAProvider) DeepCopy() *YandexLockboxCAProvider {
if in == nil {
return nil
}
out := new(YandexLockboxCAProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YandexLockboxProvider) DeepCopyInto(out *YandexLockboxProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
if in.CAProvider != nil {
in, out := &in.CAProvider, &out.CAProvider
*out = new(YandexLockboxCAProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxProvider.

View file

@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*

View file

@ -2,8 +2,8 @@ apiVersion: v2
name: external-secrets
description: External secret management for Kubernetes
type: application
version: "0.3.8"
appVersion: "v0.3.8"
version: "0.3.11"
appVersion: "v0.3.11"
kubeVersion: ">= 1.11.0-0"
keywords:
- kubernetes-external-secrets

View file

@ -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.8](https://img.shields.io/badge/Version-0.3.8-informational?style=flat-square)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.11](https://img.shields.io/badge/Version-0.3.11-informational?style=flat-square)
External secret management for Kubernetes
@ -35,6 +35,8 @@ The command removes all the Kubernetes components associated with the chart and
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| concurrent | int | `1` | Specifies the number of concurrent ExternalSecret Reconciles external-secret executes at a time. |
| deploymentAnnotations | object | `{}` | Annotations to add to Deployment |
| extraArgs | object | `{}` | |
| extraEnv | list | `[]` | |
| fullnameOverride | string | `""` | |
@ -46,7 +48,7 @@ The command removes all the Kubernetes components associated with the chart and
| leaderElect | bool | `false` | If true, external-secrets will perform leader election between instances to ensure no more than one instance of external-secrets operates at a time. |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
| podAnnotations | object | `{}` | Annotations to add to Pod |
| podLabels | object | `{}` | |
| podSecurityContext | object | `{}` | |
| priorityClassName | string | `""` | Pod priority class name. |

View file

@ -5,6 +5,10 @@ metadata:
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "external-secrets.labels" . | nindent 4 }}
{{- with .Values.deploymentAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
@ -39,7 +43,7 @@ spec:
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.extraArgs) }}
{{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.concurrent) (.Values.extraArgs) }}
args:
{{- if .Values.leaderElect }}
- --enable-leader-election=true
@ -47,6 +51,9 @@ spec:
{{- if .Values.scopedNamespace }}
- --namespace={{ .Values.scopedNamespace }}
{{- end }}
{{- if .Values.concurrent }}
- --concurrent={{ .Values.concurrent }}
{{- end }}
{{- range $key, $value := .Values.extraArgs }}
{{- if $value }}
- --{{ $key }}={{ $value }}

View file

@ -21,6 +21,10 @@ leaderElect: false
# provided namespace
scopedNamespace: ""
# -- Specifies the number of concurrent ExternalSecret Reconciles external-secret executes at
# a time.
concurrent: 1
serviceAccount:
# -- Specifies whether a service account should be created.
create: true
@ -40,7 +44,12 @@ extraEnv: []
## -- Map of extra arguments to pass to container.
extraArgs: {}
# -- Annotations to add to Deployment
deploymentAnnotations: {}
# -- Annotations to add to Pod
podAnnotations: {}
podLabels: {}
podSecurityContext: {}

View file

@ -310,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
@ -354,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:
@ -398,8 +411,33 @@ spec:
type: string
type: object
type: object
required:
- secretRef
workloadIdentity:
properties:
clusterLocation:
type: string
clusterName:
type: string
serviceAccountRef:
description: A reference to a ServiceAccount resource.
properties:
name:
description: The name of the ServiceAccount 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
required:
- name
type: object
required:
- clusterLocation
- clusterName
- serviceAccountRef
type: object
type: object
projectID:
description: ProjectID project where secret is located
@ -503,8 +541,8 @@ spec:
description: SecretRef to pass through sensitive information.
properties:
fingerprint:
description: projectID is an access token specific
to the secret.
description: Fingerprint is the fingerprint of the
API private key.
properties:
key:
description: The key of the entry in the Secret
@ -524,7 +562,8 @@ spec:
type: string
type: object
privatekey:
description: The Access Token is used for authentication
description: PrivateKey is the user's API Signing
Key in PEM format, used for authentication.
properties:
key:
description: The key of the entry in the Secret
@ -548,16 +587,18 @@ spec:
- secretRef
type: object
region:
description: projectID is an access token specific to the
secret.
description: Region is the region where secret is located.
type: string
tenancy:
description: projectID is an access token specific to the
secret.
description: Tenancy is the tenancy OCID where secret is located.
type: string
user:
description: User is an access OCID specific to the account.
type: string
vault:
description: Vault is the vault's OCID of the specific vault
where secret is located.
type: string
required:
- auth
type: object
@ -666,6 +707,11 @@ spec:
description: Jwt authenticates with Vault by passing role
and JWT token using the JWT/OIDC authentication method
properties:
path:
default: jwt
description: 'Path where the JWT authentication backend
is mounted in Vault, e.g: "jwt"'
type: string
role:
description: Role is a JWT role to authenticate using
the JWT/OIDC Vault authentication method
@ -692,6 +738,8 @@ spec:
the referent.
type: string
type: object
required:
- path
type: object
kubernetes:
description: Kubernetes authenticates with Vault by passing
@ -762,6 +810,11 @@ spec:
username/password pair using the LDAP authentication
method
properties:
path:
default: ldap
description: 'Path where the LDAP authentication backend
is mounted in Vault, e.g: "ldap"'
type: string
secretRef:
description: SecretRef to a key in a Secret resource
containing password for the LDAP user used to authenticate
@ -790,6 +843,7 @@ spec:
method
type: string
required:
- path
- username
type: object
tokenSecretRef:
@ -833,7 +887,6 @@ spec:
type.
type: string
namespace:
default: Default
description: The namespace the Provider type is in.
type: string
type:
@ -845,7 +898,6 @@ spec:
type: string
required:
- name
- namespace
- type
type: object
namespace:
@ -875,9 +927,108 @@ spec:
type: string
required:
- auth
- path
- server
type: object
webhook:
description: Webhook configures this store to sync secrets using
a generic templated webhook
properties:
body:
description: Body
type: string
caBundle:
description: PEM encoded CA bundle used to validate webhook
server certificate. Only used if the Server URL is using
HTTPS protocol. This parameter is ignored for plain HTTP
protocol connection. If not set the system root certificates
are used to validate the TLS connection.
format: byte
type: string
caProvider:
description: The provider for the CA bundle to use to validate
webhook server certificate.
properties:
key:
description: The key the value inside of the provider
type to use, only used with "Secret" type
type: string
name:
description: The name of the object located at the provider
type.
type: string
namespace:
description: The namespace the Provider type is in.
type: string
type:
description: The type of provider to use such as "Secret",
or "ConfigMap".
enum:
- Secret
- ConfigMap
type: string
required:
- name
- type
type: object
headers:
additionalProperties:
type: string
description: Headers
type: object
method:
description: Webhook Method
type: string
result:
description: Result formatting
properties:
jsonPath:
description: Json path of return value
type: string
type: object
secrets:
description: Secrets to fill in templates These secrets will
be passed to the templating function as key value pairs
under the given name
items:
properties:
name:
description: Name of this secret in templates
type: string
secretRef:
description: Secret ref to fill in credentials
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
required:
- name
- secretRef
type: object
type: array
timeout:
description: Timeout
type: string
url:
description: Webhook url to call
type: string
required:
- result
- url
type: object
yandexlockbox:
description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider
@ -908,10 +1059,44 @@ spec:
type: string
type: object
type: object
caProvider:
description: The provider for the CA bundle to use to validate
Yandex.Cloud server certificate.
properties:
certSecretRef:
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:
- auth
type: object
type: object
retrySettings:
description: Used to configure http retries if failed
properties:
maxRetries:
format: int32
type: integer
retryInterval:
type: string
type: object
required:
- provider
type: object

View file

@ -310,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
@ -354,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:
@ -398,8 +411,33 @@ spec:
type: string
type: object
type: object
required:
- secretRef
workloadIdentity:
properties:
clusterLocation:
type: string
clusterName:
type: string
serviceAccountRef:
description: A reference to a ServiceAccount resource.
properties:
name:
description: The name of the ServiceAccount 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
required:
- name
type: object
required:
- clusterLocation
- clusterName
- serviceAccountRef
type: object
type: object
projectID:
description: ProjectID project where secret is located
@ -503,8 +541,8 @@ spec:
description: SecretRef to pass through sensitive information.
properties:
fingerprint:
description: projectID is an access token specific
to the secret.
description: Fingerprint is the fingerprint of the
API private key.
properties:
key:
description: The key of the entry in the Secret
@ -524,7 +562,8 @@ spec:
type: string
type: object
privatekey:
description: The Access Token is used for authentication
description: PrivateKey is the user's API Signing
Key in PEM format, used for authentication.
properties:
key:
description: The key of the entry in the Secret
@ -548,16 +587,18 @@ spec:
- secretRef
type: object
region:
description: projectID is an access token specific to the
secret.
description: Region is the region where secret is located.
type: string
tenancy:
description: projectID is an access token specific to the
secret.
description: Tenancy is the tenancy OCID where secret is located.
type: string
user:
description: User is an access OCID specific to the account.
type: string
vault:
description: Vault is the vault's OCID of the specific vault
where secret is located.
type: string
required:
- auth
type: object
@ -666,6 +707,11 @@ spec:
description: Jwt authenticates with Vault by passing role
and JWT token using the JWT/OIDC authentication method
properties:
path:
default: jwt
description: 'Path where the JWT authentication backend
is mounted in Vault, e.g: "jwt"'
type: string
role:
description: Role is a JWT role to authenticate using
the JWT/OIDC Vault authentication method
@ -692,6 +738,8 @@ spec:
the referent.
type: string
type: object
required:
- path
type: object
kubernetes:
description: Kubernetes authenticates with Vault by passing
@ -762,6 +810,11 @@ spec:
username/password pair using the LDAP authentication
method
properties:
path:
default: ldap
description: 'Path where the LDAP authentication backend
is mounted in Vault, e.g: "ldap"'
type: string
secretRef:
description: SecretRef to a key in a Secret resource
containing password for the LDAP user used to authenticate
@ -790,6 +843,7 @@ spec:
method
type: string
required:
- path
- username
type: object
tokenSecretRef:
@ -833,7 +887,6 @@ spec:
type.
type: string
namespace:
default: Default
description: The namespace the Provider type is in.
type: string
type:
@ -845,7 +898,6 @@ spec:
type: string
required:
- name
- namespace
- type
type: object
namespace:
@ -875,9 +927,108 @@ spec:
type: string
required:
- auth
- path
- server
type: object
webhook:
description: Webhook configures this store to sync secrets using
a generic templated webhook
properties:
body:
description: Body
type: string
caBundle:
description: PEM encoded CA bundle used to validate webhook
server certificate. Only used if the Server URL is using
HTTPS protocol. This parameter is ignored for plain HTTP
protocol connection. If not set the system root certificates
are used to validate the TLS connection.
format: byte
type: string
caProvider:
description: The provider for the CA bundle to use to validate
webhook server certificate.
properties:
key:
description: The key the value inside of the provider
type to use, only used with "Secret" type
type: string
name:
description: The name of the object located at the provider
type.
type: string
namespace:
description: The namespace the Provider type is in.
type: string
type:
description: The type of provider to use such as "Secret",
or "ConfigMap".
enum:
- Secret
- ConfigMap
type: string
required:
- name
- type
type: object
headers:
additionalProperties:
type: string
description: Headers
type: object
method:
description: Webhook Method
type: string
result:
description: Result formatting
properties:
jsonPath:
description: Json path of return value
type: string
type: object
secrets:
description: Secrets to fill in templates These secrets will
be passed to the templating function as key value pairs
under the given name
items:
properties:
name:
description: Name of this secret in templates
type: string
secretRef:
description: Secret ref to fill in credentials
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
required:
- name
- secretRef
type: object
type: array
timeout:
description: Timeout
type: string
url:
description: Webhook url to call
type: string
required:
- result
- url
type: object
yandexlockbox:
description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider
@ -908,10 +1059,44 @@ spec:
type: string
type: object
type: object
caProvider:
description: The provider for the CA bundle to use to validate
Yandex.Cloud server certificate.
properties:
certSecretRef:
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:
- auth
type: object
type: object
retrySettings:
description: Used to configure http retries if failed
properties:
maxRetries:
format: int32
type: integer
retryInterval:
type: string
type: object
required:
- provider
type: object

View file

@ -22,6 +22,6 @@ Now, when creating our ExternalSecret resource, instead of using the data field,
To check both values we can run:
```
kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.username}' | base64 -d
kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.surname}' | base64 -d
```
kubectl get secret secret-to-be-created -n <namespace> -o jsonpath='{.data.username}' | base64 -d
kubectl get secret secret-to-be-created -n <namespace> -o jsonpath='{.data.surname}' | base64 -d
```

View file

@ -77,6 +77,10 @@ Events: <none>
For more advanced examples, please read the other
[guides](guides-introduction.md).
## Installing with OLM
External-secrets can be managed by [Operator Lifecycle Manager](https://olm.operatorframework.io/) (OLM) via an installer operator. It is made available through [OperatorHub.io](https://operatorhub.io/), this installation method is suited best for OpenShift. See installation instructions on the [external-secrets-operator](https://operatorhub.io/operator/external-secrets-operator) package.
## Uninstalling
Before continuing, ensure that all external-secret resources that have been created by users have been deleted.

View file

@ -0,0 +1,133 @@
# GitOps using FluxCD (v2)
FluxCD is a GitOps operator for Kubernetes. It synchronizes the status of the cluster from manifests allocated in
different repositories (Git or Helm). This approach fits perfectly with External Secrets on clusters which are dynamically
created, to get credentials with no manual intervention from the beginning.
## Advantages
This approach has several advantages as follows:
* **Homogenize environments** allowing developers to use the same toolset in Kind in the same way they do in the cloud
provider distributions such as EKS or GKE. This accelerates the development
* **Reduce security risks**, because credentials can be easily obtained, so temptation to store them locally is reduced.
* **Application compatibility increase**: Applications are deployed in different ways, and sometimes they need to share
credentials. This can be done using External Secrets as a wire for them at real time.
* **Automation by default** oh, come on!
## The approach
FluxCD is composed by several controllers dedicated to manage different custom resources. The most important
ones are **Kustomization** (to clarify, Flux one, not Kubernetes' one) and **HelmRelease** to deploy using the approaches
of the same names.
External Secrets can be deployed using Helm [as explained here](guides-getting-started.md). The deployment includes the
CRDs if enabled on the `values.yaml`, but after this, you need to deploy some `SecretStore` to start
getting credentials from your secrets manager with External Secrets.
> The idea of this guide is to deploy the whole stack, using flux, needed by developers not to worry about the credentials,
> but only about the application and its code.
## The problem
This can sound easy, but External Secrets is deployed using Helm, which is managed by the HelmController,
and your custom resources, for example a `ClusterSecretStore` and the related `Secret`, are often deployed using a
`kustomization.yaml`, which is deployed by the KustomizeController.
Both controllers manage the resources independently, at different moments, with no possibility to wait each other.
This means that we have a wonderful race condition where sometimes the CRs (`SecretStore`,`ClusterSecretStore`...) tries
to be deployed before than the CRDs needed to recognize them.
## The solution
Let's see the conditions to start working on a solution:
* The External Secrets operator is deployed with Helm, and admits disabling the CRDs deployment
* The race condition only affects the deployment of `CustomResourceDefinition` and the CRs needed later
* CRDs can be deployed directly from the Git repository of the project using a Flux `Kustomization`
* Required CRs can be deployed using a Flux `Kustomization` too, allowing dependency between CRDs and CRs
* All previous manifests can be applied with a Kubernetes `kustomization`
## Create the main kustomization
To have a better view of things needed later, the first manifest to be created is the `kustomization.yaml`
```yaml
{% include 'gitops/kustomization.yaml' %}
```
## Create the secret
To access your secret manager, External Secrets needs some credentials. They are stored inside a Secret, which is intended
to be deployed by automation as a good practise. This time, a placeholder called `secret-token.yaml` is show as an example:
```yaml
# The namespace.yaml first
{% include 'gitops/namespace.yaml' %}
```
```yaml
{% include 'gitops/secret-token.yaml' %}
```
## Creating the references to repositories
Create a manifest called `repositories.yaml` to store the references to external repositories for Flux
```yaml
{% include 'gitops/repositories.yaml' %}
```
## Deploy the CRDs
As mentioned, CRDs can be deployed using the official Helm package, but to solve the race condition, they will be deployed
from our git repository using a Kustomization manifest called `deployment-crds.yaml` as follows:
```yaml
{% include 'gitops/deployment-crds.yaml' %}
```
## Deploy the operator
The operator is deployed using a HelmRelease manifest to deploy the Helm package, but due to the special race condition,
the deployment must be disabled in the `values` of the manifest called `deployment.yaml`, as follows:
```yaml
{% include 'gitops/deployment.yaml' %}
```
## Deploy the CRs
Now, be ready for the arcane magic. Create a Kustomization manifest called `deployment-crs.yaml` with the following content:
```yaml
{% include 'gitops/deployment-crs.yaml' %}
```
There are several interesting details to see here, that finally solves the race condition:
1. First one is the field `dependsOn`, which points to a previous Kustomization called `external-secrets-crds`. This
dependency forces this deployment to wait for the other to be ready, before start being deployed.
2. The reference to the place where to find the CRs
```yaml
path: ./infrastructure/external-secrets/crs
sourceRef:
kind: GitRepository
name: flux-system
```
Custom Resources will be searched in the relative path `./infrastructure/external-secrets/crs` of the GitRepository
called `flux-system`, which is a reference to the same repository that FluxCD watches to synchronize the cluster.
With fewer words, a reference to itself, but going to another directory called `crs`
Of course, allocate inside the mentioned path `./infrastructure/external-secrets/crs`, all the desired CRs to be deployed,
for example, a manifest `clusterSecretStore.yaml` to reach your Hashicorp Vault as follows:
```yaml
{% include 'gitops/crs/clusterSecretStore.yaml' %}
```
## Results
At the end, the required files tree is shown in the following picture:
![FluxCD files tree](./pictures/screenshot_gitops_final_directory_tree.png)

View file

@ -0,0 +1,26 @@
You can test a feature that was not yet released using the following method, use it at your own discretion:
1. Create a `values.yaml` file with the following content:
```
replicaCount: 1
image:
repository: ghcr.io/external-secrets/external-secrets
pullPolicy: IfNotPresent
# -- The image tag to use. The default is the chart appVersion.
tag: "main"
# -- If set, install and upgrade CRDs through helm chart.
installCRDs: false
```
2. Install the crds
```
make crds.install
```
3. Install the external-secrets Helm chart indicating the values file created before:
```
helm install external-secrets external-secrets/external-secrets -f values.yaml
```

View file

@ -25,7 +25,7 @@ lifecycle of the secrets for you.
To get started, please read through [API overview](api-overview.md) this should
give you a high-level overview to understand the API and use-cases. After that
please follow one of our [guides](guides-introduction.md) to get a jump start
using the operator.
using the operator. See our [getting started guide](guides-getting-started.md) for installation instructions.
For a complete reference of the API types please refer to our [API
Reference](spec.md).

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -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).

View file

@ -2,39 +2,69 @@
External Secrets Operator integrates with [GCP Secret Manager](https://cloud.google.com/secret-manager) for secret management.
### Service account key authentication
## Authentication
A service account key is created and the JSON keyfile is stored in a `Kind=Secret`. The `project_id` and `private_key` should be configured for the project.
### Workload Identity
Your Google Kubernetes Engine (GKE) applications can consume GCP services like Secrets Manager without using static, long-lived authentication tokens. This is our recommended approach of handling credentials in GCP. ESO offers two options for integrating with GKE workload identity: **pod-based workload identity** and **using service accounts directly**. Before using either way you need to create a service account - this is covered below.
#### Creating Workload Identity Service Accounts
You can find the documentation for Workload Identity [here](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here.
Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values:
- `CLUSTER_NAME`: The name of your cluster
- `PROJECT_ID`: Your project ID (not your Project number nor your Project name)
- `K8S_NAMESPACE`: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator
- `KSA_NAME`: external-secrets (if you are not creating a new one to attach to the deployemnt)
- `GSA_NAME`: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources
- `ROLE_NAME`: should be `roles/secretmanager.secretAccessor` - so you make the pod only be able to access secrets on Secret Manager
#### Using Service Accounts directly
Let's assume you have created a service account correctly and attached a appropriate workload identity. It should roughly look like this:
```yaml
{% include 'gcpsm-credentials-secret.yaml' %}
apiVersion: v1
kind: ServiceAccount
metadata:
name: team-a
namespace: team-a
annotations:
iam.gke.io/gcp-service-account: example-team-a@my-project.iam.gserviceaccount.com
```
### Update secret store
Be sure the `gcpsm` provider is listed in the `Kind=SecretStore`
You can reference this particular ServiceAccount in a `SecretStore` or `ClusterSecretStore`. It's important that you also set the `projectID`, `clusterLocation` and `clusterName`. The Namespace on the `serviceAccountRef` is ignored when using a `SecretStore` resource. This is needed to isolate the namespaces properly.
```yaml
{% include 'gcpsm-secret-store.yaml' %}
apiVersion: external-secrets.io/v1alpha1
kind: ClusterSecretStore
metadata:
name: gcp-wi
spec:
provider:
gcpsm:
projectID: my-project
auth:
workloadIdentity:
# name of the cluster region
clusterLocation: europe-central2
# name of the GKE cluster
clusterName: example-workload-identity
# reference the sa from above
serviceAccountRef:
name: team-a
namespace: team-a
```
### Creating external secret
*You need to give the Google service account the `roles/iam.serviceAccountTokenCreator` role so it can generate a service account token for you (not necessary in the Pod-based Workload Identity bellow)*
To create a kubernetes secret from the GCP Secret Manager secret a `Kind=ExternalSecret` is needed.
#### Using Pod-based Workload Identity
```yaml
{% include 'gcpsm-external-secret.yaml' %}
```
You can attach a Workload Identity directly to the ESO pod. ESO then has access to all the APIs defined in the attached service account policy. You attach the workload identity by (1) creating a service account with a attached workload identity (described above) and (2) using this particular service account in the pod's `serviceAccountName` field.
The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=Secret`
```
kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
```
## Authentication with Workload Identity
This makes it possible for your Google Kubernetes Engine (GKE) applications to consume services provided by Google APIs, namely Secrets Manager service in this case.
Here we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like:
For this example we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like:
```sh
helm install external-secrets external-secrets/external-secrets --namespace es
@ -42,7 +72,7 @@ helm install external-secrets external-secrets/external-secrets --namespace es
Then most of the resources would have this name, the important one here being the k8s service account attached to the external-secrets operator deployment:
```
```yaml
# ...
containers:
- image: ghcr.io/external-secrets/external-secrets:vVERSION
@ -56,30 +86,10 @@ Then most of the resources would have this name, the important one here being th
serviceAccountName: external-secrets # <--- here
```
### Following the documentation
The pod now has the identity. Now you need to configure the `SecretStore`.
You just need to set the `projectID`, all other fields can be omitted.
You can find the documentation for Workload Identity under [this url](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here.
#### Changing Values
Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values:
- CLUSTER_NAME: The name of your cluster
- PROJECT_ID: Your project ID (not your Project number nor your Project name)
- K8S_NAMESPACE: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator
- KSA_NAME: external-secrets (if you are not creating a new one to attach to the deployemnt)
- GSA_NAME: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources
- ROLE_NAME: roles/secretmanager.secretAccessor so you make the pod only be able to access secrets on Secret Manager
#### Following through
You can follow through the documentation and adapt it to your specific use case. If you want to just use the serviceaccount that we deployed with the helm chart, for example, you don't need to create a new service account on 2 of [Authenticating to Google Cloud](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#authenticating_to).
#### SecretStore with WorkloadIdentity
To use workload identity you can just omit the auth field of the secret store and let the operator client fall back to defaults using the roles attached to your service account.
```
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
@ -88,4 +98,33 @@ spec:
provider:
gcpsm:
projectID: pid
```
```
### GCP Service Account authentication
You can use [GCP Service Account](https://cloud.google.com/iam/docs/service-accounts) to authenticate with GCP. These are static, long-lived credentials. A GCP Service Account is a JSON file that needs to be stored in a `Kind=Secret`. ESO will use that Secret to authenticate with GCP. See here how you [manage GCP Service Accounts](https://cloud.google.com/iam/docs/creating-managing-service-accounts).
```yaml
{% include 'gcpsm-credentials-secret.yaml' %}
```
#### Update secret store
Be sure the `gcpsm` provider is listed in the `Kind=SecretStore`
```yaml
{% include 'gcpsm-secret-store.yaml' %}
```
#### Creating external secret
To create a kubernetes secret from the GCP Secret Manager secret a `Kind=ExternalSecret` is needed.
```yaml
{% include 'gcpsm-external-secret.yaml' %}
```
The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=Secret`
```
kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
```

View file

@ -54,7 +54,7 @@ spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
kind: SecretStore
target:
name: example-sync
data:
@ -77,10 +77,12 @@ Vault supports only simple key/value pairs - nested objects are not supported. H
### Authentication
We support three different modes for authentication:
We support five different modes for authentication:
[token-based](https://www.vaultproject.io/docs/auth/token),
[appRole](https://www.vaultproject.io/docs/auth/approle) and
[kubernetes-native](https://www.vaultproject.io/docs/auth/kubernetes), each one comes with it's own
[appRole](https://www.vaultproject.io/docs/auth/approle),
[kubernetes-native](https://www.vaultproject.io/docs/auth/kubernetes),
[ldap](https://www.vaultproject.io/docs/auth/ldap) and
[jwt/odic](https://www.vaultproject.io/docs/auth/jwt), each one comes with it's own
trade-offs. Depending on the authentication method you need to adapt your environment.
#### Token-based authentication

View file

@ -32,7 +32,7 @@ This will automatically generate a fingerprint.
![API-key-details](./pictures/screenshot_API_key.png)
### Update secret store
Be sure the `oracle` provider is listed in the `Kind=SecretStore`
Be sure the `oracle` provider is listed in the `Kind=SecretStore`.
```yaml
{% include 'oracle-secret-store.yaml' %}
@ -51,4 +51,4 @@ To create a kubernetes secret from the Oracle Cloud Interface secret a`Kind=Exte
The operator will fetch the project variable and inject it as a `Kind=Secret`.
```
kubectl get secret oracle-secret-to-create -o jsonpath='{.data.dev-secret-test}' | base64 -d
```
```

120
docs/provider-webhook.md Normal file
View file

@ -0,0 +1,120 @@
## Generic Webhook
External Secrets Operator can integrate with simple web apis by specifying the endpoint
### Example
First, create a SecretStore with a webhook backend. We'll use a static user/password `root`:
```yaml
{% raw %}
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: webhook-backend
spec:
provider:
webhook:
url: "http://httpbin.org/get?parameter={{ .remoteRef.key }}"
result:
jsonPath: "$.args.parameter"
headers:
Content-Type: application/json
Authorization: Basic {{ print .auth.username ":" .auth.password | b64enc }}
secrets:
- name: auth
secretRef:
name: webhook-credentials
{%- endraw %}
---
apiVersion: v1
kind: Secret
metadata:
name: webhook-credentials
data:
username: dGVzdA== # "test"
password: dGVzdA== # "test"
```
NB: This is obviously not practical because it just returns the key as the result, but it shows how it works
Now create an ExternalSecret that uses the above SecretStore:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: webhook-example
spec:
refreshInterval: "15s"
secretStoreRef:
name: webhook-backend
kind: SecretStore
target:
name: example-sync
data:
- secretKey: foobar
remoteRef:
key: secret
---
# will create a secret with:
kind: Secret
metadata:
name: example-sync
data:
foobar: c2VjcmV0
```
#### Limitations
Webhook does not support authorization, other than what can be sent by generating http headers
### Templating
Generic WebHook provider uses the templating engine to generate the API call. It can be used in the url, headers, body and result.jsonPath fields.
The provider inserts the secret to be retrieved in the object named `remoteRef`.
In addition, secrets can be added as named objects, for example to use in authorization headers.
Each secret has a `name` property which determines the name of the object in the templating engine.
### All Parameters
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: ClusterSecretStore
metadata:
name: statervault
spec:
provider:
webhook:
# Url to call. Use templating engine to fill in the request parameters
url: <url>
# http method, defaults to GET
method: <method>
# Timeout in duration (1s, 1m, etc)
timeout: 1s
result:
# [jsonPath](https://jsonpath.com) syntax, which also can be templated
jsonPath: <jsonPath>
# Map of headers, can be templated
headers:
<Header-Name>: <header contents>
# Body to sent as request, can be templated (optional)
body: <body>
# List of secrets to expose to the templating engine
secrets:
# Use this name to refer to this secret in templating, above
- name: <name>
secretRef:
namespace: <namespace>
name: <name>
# Add CAs here for the TLS handshake
caBundle: <base64 encoded cabundle>
caProvider:
type: Secret or COnfigMap
name: <name of secret or configmap>
namespace: <namespace>
key: <key inside secret>
```

View file

@ -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: "<MI_clientId>"
# 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"

View file

@ -22,9 +22,9 @@ spec:
role: iam-role
# AWS Region to be used for the provider
region: eu-central-1
# Auth defines the information necessary to authenticate against AWS by
# getting the accessKeyID and secretAccessKey from an already created Kubernetes Secret
# Auth defines the information necessary to authenticate against AWS
auth:
# Getting the accessKeyID and secretAccessKey from an already created Kubernetes Secret
secretRef:
accessKeyID:
name: awssm-secret
@ -32,6 +32,12 @@ spec:
secretAccessKey:
name: awssm-secret
key: secret-access-key
# IAM roles for service accounts
# https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html
jwt:
serviceAccountRef:
name: my-serviceaccount
namespace: sa-namespace
vault:
server: "https://vault.acme.org"
@ -42,7 +48,17 @@ spec:
version: "v2"
# vault enterprise namespace: https://www.vaultproject.io/docs/enterprise/namespaces
namespace: "a-team"
# base64 encoded string of certificate
caBundle: "..."
# Instead of caBundle you can also specify a caProvider
# this will retrieve the cert from a Secret or ConfigMap
caProvider:
# Can be Secret or ConfigMap
type: "Secret"
# This is mandatory for ClusterSecretStore and not relevant for SecretStore
namespace: "my-cert-secret-namespace"
name: "my-cert-secret"
key: "cert-key"
auth:
# static token: https://www.vaultproject.io/docs/auth/token
tokenSecretRef:

View file

@ -11,6 +11,14 @@ spec:
# Optional
controller: dev
# You can specify retry settings for the http connection
# these fields allow you to set a maxRetries before failure, and
# an interval between the retries.
# Current supported providers: IBM
retrySettings:
maxRetries: 5
retryInterval: "10s"
# provider field contains the configuration to access the provider
# which contains the secret exactly one provider must be configured.
provider:
@ -50,8 +58,6 @@ spec:
caProvider:
# Can be Secret or ConfigMap
type: "Secret"
# This is optional, if not specified will be 'Default'
namespace: "my-cert-secret-namespace"
name: "my-cert-secret"
key: "cert-key"

View file

@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1alpha1
kind: ClusterSecretStore
metadata:
name: vault-backend-global
spec:
provider:
vault:
server: "https://vault.your-domain.com"
path: secret
version: v2
auth:
# points to a secret that contains a vault token
# https://www.vaultproject.io/docs/auth/token
tokenSecretRef:
name: "vault-token-global"
key: "token"
namespace: external-secrets

View file

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- clusterSecretStore.yaml

View file

@ -0,0 +1,13 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: external-secrets-crds
namespace: flux-system
spec:
interval: 10m
path: ./deploy/crds
prune: true
sourceRef:
kind: GitRepository
name: external-secrets

View file

@ -0,0 +1,15 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: external-secrets-crs
namespace: flux-system
spec:
dependsOn:
- name: external-secrets-crds
interval: 10m
path: ./infrastructure/external-secrets/crs
prune: true
sourceRef:
kind: GitRepository
name: flux-system

View file

@ -0,0 +1,28 @@
# How to manage values files. Ref: https://fluxcd.io/docs/guides/helmreleases/#refer-to-values-inside-the-chart
# How to inject values: https://fluxcd.io/docs/guides/helmreleases/#cloud-storage
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: external-secrets
namespace: flux-system
spec:
# Override Release name to avoid the pattern Namespace-Release
# Ref: https://fluxcd.io/docs/components/helm/api/#helm.toolkit.fluxcd.io/v2beta1.HelmRelease
releaseName: external-secrets
targetNamespace: external-secrets
interval: 10m
chart:
spec:
chart: external-secrets
version: 0.3.9
sourceRef:
kind: HelmRepository
name: external-secrets
namespace: flux-system
values:
installCRDs: false
# Ref: https://fluxcd.io/docs/components/helm/api/#helm.toolkit.fluxcd.io/v2beta1.Install
install:
createNamespace: true

View file

@ -0,0 +1,20 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Deploy the Vault access secret
- namespace.yaml
- secret-token.yaml
# Deploy the repositories
- repositories.yaml
# Deploy the CRDs
- deployment-crds.yaml
# Deploy the operator
- deployment.yaml
# Deploy default Custom Resources from 'crs' directory
# INFO: This depends on the CRDs deployment. Will happen after it
- deployment-crs.yaml

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: external-secrets

View file

@ -0,0 +1,20 @@
# Reference to Helm repository
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
name: external-secrets
namespace: flux-system
spec:
interval: 10m
url: https://charts.external-secrets.io
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
name: external-secrets
namespace: flux-system
spec:
interval: 10m
ref:
branch: main
url: http://github.com/external-secrets/external-secrets

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: vault-token-global
namespace: external-secrets
stringData:
# This token must be patched by overlays. Not here for security reasons
token: change-me-placeholder

View file

@ -10,7 +10,5 @@ spec:
target:
name: secret-to-be-created # Name for the secret on the cluster
creationPolicy: Owner
data:
- secretKey:
remoteRef:
key:
dataFrom:
- key: the-secret-name

View file

@ -5,9 +5,10 @@ metadata:
spec:
provider:
oracle: #Needs to match value in secretstore_types.go
user:
tenancy:
region:
vault: # The vault OCID
user:
tenancy:
region:
auth:
secretRef:
privatekey:
@ -15,4 +16,4 @@ spec:
key: privateKey #Needs to match stringData val in secret_oracle.yml
fingerprint:
name: oracle-secret
key: fingerprint
key: fingerprint

View file

@ -1,5 +1,5 @@
{% raw %}
# define your tempalte in a config map
# define your template in a config map
apiVersion: v1
kind: ConfigMap
metadata:

View file

@ -13,6 +13,8 @@ spec:
# VaultJwt authenticates with Vault using the JWT/OIDC auth mechanism
# https://www.vaultproject.io/docs/auth/jwt
jwt:
# Path where the JWT authentication backend is mounted
path: "jwt"
# JWT role configured in a Vault server, optional.
role: "vault-jwt-role"
secretRef:

View file

@ -13,6 +13,8 @@ spec:
# VaultLdap authenticates with Vault using the LDAP auth mechanism
# https://www.vaultproject.io/docs/auth/ldap
ldap:
# Path where the LDAP authentication backend is mounted
path: "ldap"
# LDAP username
username: "username"
secretRef:

File diff suppressed because it is too large Load diff

BIN
e2e/.DS_Store vendored

Binary file not shown.

View file

@ -1,4 +1,4 @@
ARG GO_VERSION=1.16
ARG GO_VERSION=1.17
FROM golang:$GO_VERSION-buster as builder
ENV KUBECTL_VERSION="v1.21.2"
@ -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.14.2
FROM alpine:3.15.0
RUN apk add -U --no-cache \
ca-certificates \
bash \

View file

@ -6,8 +6,12 @@ IMG_TAG = test
IMG = local/external-secrets-e2e:$(IMG_TAG)
KIND_IMG = "kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6"
BUILD_ARGS ?=
IMAGE_REGISTRY ?=
export FOCUS := $(FOCUS)
export E2E_IMAGE_REGISTRY ?=
export E2E_VERSION ?=
start-kind: ## Start kind cluster
kind create cluster \
--name external-secrets \
@ -25,6 +29,25 @@ test: e2e-image ## Run e2e tests against current kube context
kind load docker-image --name="external-secrets" $(IMG)
./run.sh
test.managed: e2e-remote-values e2e-image.managed ## Run e2e tests against current kube context
$(MAKE) -C ../ docker.build \
VERSION=$(PR_IMG_TAG) \
ARCH=amd64 \
BUILD_ARGS="${BUILD_ARGS} --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux"
$(MAKE) -C ../ docker.push \
VERSION=$(PR_IMG_TAG)
$(MAKE) -C ../ docker.push \
IMAGE_REGISTRY=$(E2E_IMAGE_REGISTRY) \
VERSION=$(E2E_VERSION)
./run.sh
e2e-remote-values:
sed -i "s|repository: [^ ]*|repository: $(IMAGE_REGISTRY)|g" k8s/eso.values.yaml
sed -i "s|tag: [^ ]*|tag: $(PR_IMG_TAG)|g" k8s/eso.values.yaml
sed -i "s|repository: [^ ]*|repository: $(IMAGE_REGISTRY)|g" k8s/eso.scoped.values.yaml
sed -i "s|tag: [^ ]*|tag: $(PR_IMG_TAG)|g" k8s/eso.scoped.values.yaml
e2e-bin:
CGO_ENABLED=0 go run github.com/onsi/ginkgo/ginkgo build .
@ -35,6 +58,13 @@ e2e-image: e2e-bin
cp -r ../deploy ./k8s
docker build $(BUILD_ARGS) -t $(IMG) .
e2e-image.managed: e2e-bin
-rm -rf ./k8s/deploy
mkdir -p k8s
$(MAKE) -C ../ helm.generate
cp -r ../deploy ./k8s
docker build $(BUILD_ARGS) -t ghcr.io/external-secrets/external-secrets-e2e:$(E2E_VERSION) .
stop-kind: ## Stop kind cluster
kind delete cluster \
--name external-secrets \

View file

@ -21,7 +21,6 @@ import (
// nolint
. "github.com/onsi/gomega"
"github.com/external-secrets/external-secrets/e2e/framework"
"github.com/external-secrets/external-secrets/e2e/framework/addon"
"github.com/external-secrets/external-secrets/e2e/framework/util"
_ "github.com/external-secrets/external-secrets/e2e/suite"
@ -29,7 +28,7 @@ import (
var _ = SynchronizedBeforeSuite(func() []byte {
cfg := &addon.Config{}
cfg.KubeConfig, cfg.KubeClientSet, cfg.CRClient = framework.NewConfig()
cfg.KubeConfig, cfg.KubeClientSet, cfg.CRClient = util.NewConfig()
By("installing localstack")
addon.InstallGlobalAddon(addon.NewLocalstack(), cfg)

View file

@ -13,6 +13,19 @@ limitations under the License.
*/
package addon
import (
"fmt"
"os"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/gomega"
// nolint
"github.com/external-secrets/external-secrets/e2e/framework/util"
)
type ESO struct {
Addon
}
@ -28,6 +41,58 @@ func NewESO() *ESO {
}
}
func (l *ESO) Install() error {
By("Installing eso\n")
err := l.Addon.Install()
if err != nil {
return err
}
By("afterInstall eso\n")
err = l.afterInstall()
if err != nil {
return err
}
return nil
}
func (l *ESO) afterInstall() error {
err := gcpPreparation()
Expect(err).NotTo(HaveOccurred())
err = awsPreparation()
Expect(err).NotTo(HaveOccurred())
if err != nil {
return err
}
return nil
}
func gcpPreparation() error {
gcpProjectID := os.Getenv("GCP_PROJECT_ID")
gcpGSAName := os.Getenv("GCP_GSA_NAME")
gcpKSAName := os.Getenv("GCP_KSA_NAME")
_, kubeClientSet, _ := util.NewConfig()
annotations := make(map[string]string)
annotations["iam.gke.io/gcp-service-account"] = fmt.Sprintf("%s@%s.iam.gserviceaccount.com", gcpGSAName, gcpProjectID)
_, err := util.UpdateKubeSA(gcpKSAName, kubeClientSet, "default", annotations)
Expect(err).NotTo(HaveOccurred())
_, err = util.UpdateKubeSA("external-secrets-e2e", kubeClientSet, "default", annotations)
Expect(err).NotTo(HaveOccurred())
if err != nil {
return err
}
return nil
}
func awsPreparation() error {
return nil
}
func NewScopedESO() *ESO {
return &ESO{
&HelmChart{

View file

@ -57,6 +57,7 @@ type Vault struct {
JWTPrivKey []byte
JWTToken string
JWTRole string
JWTPath string
KubernetesAuthPath string
KubernetesAuthRole string
@ -160,6 +161,7 @@ func (l *Vault) initVault() error {
l.JWTPrivKey = jwtPrivkey
l.JWTPubkey = jwtPubkey
l.JWTToken = jwtToken
l.JWTPath = "myjwt" // see configure-vault.sh
l.JWTRole = "external-secrets-operator" // see configure-vault.sh
l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh
l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh

View file

@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/e2e/framework/log"
)
// WaitForSecretValue waits until a secret comes into existence and compares the secret.Data
@ -38,6 +39,7 @@ func (f *Framework) WaitForSecretValue(namespace, name string, expected *v1.Secr
Name: name,
}, secret)
if apierrors.IsNotFound(err) {
log.Logf("Secret Not Found. Expected: %+v, Got: %+v", expected, secret)
return false, nil
}
return equalSecrets(expected, secret), nil

View file

@ -14,19 +14,18 @@ limitations under the License.
package framework
import (
"os"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/gomega"
// nolint
. "github.com/onsi/ginkgo/extensions/table"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
kscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@ -34,11 +33,9 @@ import (
"github.com/external-secrets/external-secrets/e2e/framework/util"
)
var Scheme = runtime.NewScheme()
func init() {
_ = kscheme.AddToScheme(Scheme)
_ = esv1alpha1.AddToScheme(Scheme)
_ = kscheme.AddToScheme(util.Scheme)
_ = esv1alpha1.AddToScheme(util.Scheme)
}
type Framework struct {
@ -64,7 +61,7 @@ func New(baseName string) *Framework {
f := &Framework{
BaseName: baseName,
}
f.KubeConfig, f.KubeClientSet, f.CRClient = NewConfig()
f.KubeConfig, f.KubeClientSet, f.CRClient = util.NewConfig()
BeforeEach(f.BeforeEach)
AfterEach(f.AfterEach)
@ -110,27 +107,17 @@ func (f *Framework) Install(a addon.Addon) {
Expect(err).NotTo(HaveOccurred())
}
// NewConfig loads and returns the kubernetes credentials from the environment.
// KUBECONFIG env var takes precedence and falls back to in-cluster config.
func NewConfig() (*rest.Config, *kubernetes.Clientset, crclient.Client) {
var kubeConfig *rest.Config
var err error
kcPath := os.Getenv("KUBECONFIG")
if kcPath != "" {
kubeConfig, err = clientcmd.BuildConfigFromFlags("", kcPath)
Expect(err).NotTo(HaveOccurred())
} else {
kubeConfig, err = rest.InClusterConfig()
Expect(err).NotTo(HaveOccurred())
// Compose helps define multiple testcases with same/different auth methods.
func Compose(descAppend string, f *Framework, fn func(f *Framework) (string, func(*TestCase)), tweaks ...func(*TestCase)) TableEntry {
desc, tfn := fn(f)
tweaks = append(tweaks, tfn)
te := Entry(desc + " " + descAppend)
// need to convert []func to []interface{}
ifs := make([]interface{}, len(tweaks))
for i := 0; i < len(tweaks); i++ {
ifs[i] = tweaks[i]
}
By("creating a kubernetes client")
kubeClientSet, err := kubernetes.NewForConfig(kubeConfig)
Expect(err).NotTo(HaveOccurred())
By("creating a controller-runtime client")
CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: Scheme})
Expect(err).NotTo(HaveOccurred())
return kubeConfig, kubeClientSet, CRClient
te.Parameters = ifs
return te
}

View file

@ -22,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/e2e/framework/log"
)
var TargetSecretName = "target-secret"
@ -72,7 +73,11 @@ func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase))
}
// wait for Kind=Secret to have the expected data
_, err = tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
secret, err := tc.Framework.WaitForSecretValue(tc.Framework.Namespace.Name, TargetSecretName, tc.ExpectedSecret)
if err != nil {
log.Logf("Did not match. Expected: %+v, Got: %+v", tc.ExpectedSecret, secret)
}
Expect(err).ToNot(HaveOccurred())
}
}

View file

@ -18,18 +18,28 @@ import (
"context"
"fmt"
"net/http"
"os"
"time"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
)
var Scheme = runtime.NewScheme()
const (
// How often to poll for conditions.
Poll = 2 * time.Second
@ -206,3 +216,45 @@ func WaitForURL(url string) error {
return false, err
})
}
// UpdateKubeSA updates a new Kubernetes Service Account for a test.
func UpdateKubeSA(baseName string, kubeClientSet kubernetes.Interface, ns string, annotations map[string]string) (*v1.ServiceAccount, error) {
sa := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: baseName,
Annotations: annotations,
},
}
return kubeClientSet.CoreV1().ServiceAccounts(ns).Update(context.TODO(), sa, metav1.UpdateOptions{})
}
// UpdateKubeSA updates a new Kubernetes Service Account for a test.
func GetKubeSA(baseName string, kubeClientSet kubernetes.Interface, ns string) (*v1.ServiceAccount, error) {
return kubeClientSet.CoreV1().ServiceAccounts(ns).Get(context.TODO(), baseName, metav1.GetOptions{})
}
// NewConfig loads and returns the kubernetes credentials from the environment.
// KUBECONFIG env var takes precedence and falls back to in-cluster config.
func NewConfig() (*restclient.Config, *kubernetes.Clientset, crclient.Client) {
var kubeConfig *restclient.Config
var err error
kcPath := os.Getenv("KUBECONFIG")
if kcPath != "" {
kubeConfig, err = clientcmd.BuildConfigFromFlags("", kcPath)
Expect(err).NotTo(HaveOccurred())
} else {
kubeConfig, err = restclient.InClusterConfig()
Expect(err).NotTo(HaveOccurred())
}
By("creating a kubernetes client")
kubeClientSet, err := kubernetes.NewForConfig(kubeConfig)
Expect(err).NotTo(HaveOccurred())
By("creating a controller-runtime client")
CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: Scheme})
Expect(err).NotTo(HaveOccurred())
return kubeConfig, kubeClientSet, CRClient
}

View file

@ -51,17 +51,17 @@ vault write auth/myapprole/role/eso-e2e-role \
secret_id_num_uses=40
# ------------------
# App Role AUTH
# JWT AUTH
# https://www.vaultproject.io/docs/auth/jwt
# ------------------
vault auth enable jwt
vault auth enable -path=myjwt jwt
vault write auth/jwt/config \
vault write auth/myjwt/config \
jwt_validation_pubkeys=@/etc/vault-config/jwt-pubkey.pem \
bound_issuer="example.iss" \
default_role="external-secrets-operator"
vault write auth/jwt/role/external-secrets-operator \
vault write auth/myjwt/role/external-secrets-operator \
role_type="jwt" \
bound_subject="vault@example" \
bound_audiences="vault.client" \

View file

@ -25,7 +25,6 @@ cd $DIR
echo "Kubernetes cluster:"
kubectl get nodes -o wide
kubectl describe node external-secrets-control-plane
echo -e "Granting permissions to e2e service account..."
kubectl create serviceaccount external-secrets-e2e || true
@ -52,6 +51,11 @@ kubectl run --rm \
--env="FOCUS=${FOCUS:-.*}" \
--env="GCP_SM_SA_JSON=${GCP_SM_SA_JSON:-}" \
--env="GCP_PROJECT_ID=${GCP_PROJECT_ID:-}" \
--env="TF_VAR_GCP_PROJECT_ID=${TF_VAR_GCP_PROJECT_ID:-}" \
--env="GCP_GSA_NAME=${GCP_GSA_NAME:-}" \
--env="GCP_KSA_NAME=${GCP_KSA_NAME:-}" \
--env="TF_VAR_GCP_GSA_NAME=${TF_VAR_GCP_GSA_NAME:-}" \
--env="TF_VAR_GCP_KSA_NAME=${TF_VAR_GCP_KSA_NAME:-}" \
--env="AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}" \
--env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \
--env="AKEYLESS_ACCESS_ID=${AKEYLESS_ACCESS_ID:-}" \
@ -67,4 +71,4 @@ kubectl run --rm \
--env="ORACLE_FINGERPRINT=${ORACLE_FINGERPRINT:-}" \
--env="ORACLE_KEY=${ORACLE_KEY:-}" \
--overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \
e2e --image=local/external-secrets-e2e:test
e2e --image=${E2E_IMAGE_REGISTRY}:${E2E_VERSION}

View file

@ -36,10 +36,10 @@ var _ = Describe("[gcp] ", func() {
f := framework.New("eso-gcp")
credentials := os.Getenv("GCP_SM_SA_JSON")
projectID := os.Getenv("GCP_PROJECT_ID")
prov := &gcpProvider{}
prov := &GcpProvider{}
if credentials != "" && projectID != "" {
prov = newgcpProvider(f, credentials, projectID)
prov = NewgcpProvider(f, credentials, projectID, "", "", "", "")
}
// P12Cert case creates a secret with a p12 cert containing a privkey and cert bundled together.

View file

@ -23,40 +23,108 @@ import (
// nolint
. "github.com/onsi/gomega"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"google.golang.org/api/option"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
utilpointer "k8s.io/utils/pointer"
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"
"github.com/external-secrets/external-secrets/e2e/framework/log"
gcpsm "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
)
type gcpProvider struct {
credentials string
projectID string
framework *framework.Framework
const (
PodIDSecretStoreName = "pod-identity"
SpecifcSASecretStoreName = "specific-sa"
)
func makeStore(s *GcpProvider) *esv1alpha1.SecretStore {
return &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: s.framework.Namespace.Name,
Namespace: s.framework.Namespace.Name,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
GCPSM: &esv1alpha1.GCPSMProvider{
ProjectID: s.projectID,
},
},
},
}
}
func newgcpProvider(f *framework.Framework, credentials, projectID string) *gcpProvider {
prov := &gcpProvider{
credentials: credentials,
projectID: projectID,
framework: f,
func makeCStore(s *GcpProvider) *esv1alpha1.ClusterSecretStore {
return &esv1alpha1.ClusterSecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: s.framework.Namespace.Name,
Namespace: s.framework.Namespace.Name,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
GCPSM: &esv1alpha1.GCPSMProvider{
ProjectID: s.projectID,
},
},
},
}
}
// nolint // Better to keep names consistent even if it stutters;
type GcpProvider struct {
credentials string
projectID string
framework *framework.Framework
clusterLocation string
clusterName string
serviceAccountName string
serviceAccountNamespace string
}
func NewgcpProvider(f *framework.Framework, credentials, projectID string,
clusterLocation string, clusterName string, serviceAccountName string, serviceAccountNamespace string) *GcpProvider {
prov := &GcpProvider{
credentials: credentials,
projectID: projectID,
framework: f,
clusterLocation: clusterLocation,
clusterName: clusterName,
serviceAccountName: serviceAccountName,
serviceAccountNamespace: serviceAccountNamespace,
}
BeforeEach(prov.BeforeEach)
return prov
}
func (s *gcpProvider) CreateSecret(key, val string) {
func (s *GcpProvider) getClient(ctx context.Context, credentials string) (client *secretmanager.Client, err error) {
if credentials == "" {
var ts oauth2.TokenSource
ts, err = google.DefaultTokenSource(ctx, gcpsm.CloudPlatformRole)
Expect(err).ToNot(HaveOccurred())
client, err = secretmanager.NewClient(ctx, option.WithTokenSource(ts))
Expect(err).ToNot(HaveOccurred())
} else {
var config *jwt.Config
config, err = google.JWTConfigFromJSON([]byte(s.credentials), gcpsm.CloudPlatformRole)
Expect(err).ToNot(HaveOccurred())
ts := config.TokenSource(ctx)
client, err = secretmanager.NewClient(ctx, option.WithTokenSource(ts))
Expect(err).ToNot(HaveOccurred())
}
return client, err
}
func (s *GcpProvider) CreateSecret(key, val string) {
ctx := context.Background()
config, err := google.JWTConfigFromJSON([]byte(s.credentials), gcpsm.CloudPlatformRole)
Expect(err).ToNot(HaveOccurred())
ts := config.TokenSource(ctx)
client, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
client, err := s.getClient(ctx, s.credentials)
Expect(err).ToNot(HaveOccurred())
defer client.Close()
// Create the request to create the secret.
@ -83,12 +151,10 @@ func (s *gcpProvider) CreateSecret(key, val string) {
Expect(err).ToNot(HaveOccurred())
}
func (s *gcpProvider) DeleteSecret(key string) {
func (s *GcpProvider) DeleteSecret(key string) {
ctx := context.Background()
config, err := google.JWTConfigFromJSON([]byte(s.credentials), gcpsm.CloudPlatformRole)
client, err := s.getClient(ctx, s.credentials)
Expect(err).ToNot(HaveOccurred())
ts := config.TokenSource(ctx)
client, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
Expect(err).ToNot(HaveOccurred())
defer client.Close()
req := &secretmanagerpb.DeleteSecretRequest{
@ -98,7 +164,7 @@ func (s *gcpProvider) DeleteSecret(key string) {
Expect(err).ToNot(HaveOccurred())
}
func (s *gcpProvider) BeforeEach() {
func (s *GcpProvider) BeforeEach() {
By("creating a gcp secret")
gcpCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
@ -110,30 +176,60 @@ func (s *gcpProvider) BeforeEach() {
},
}
err := s.framework.CRClient.Create(context.Background(), gcpCreds)
Expect(err).ToNot(HaveOccurred())
if err != nil {
err = s.framework.CRClient.Update(context.Background(), gcpCreds)
Expect(err).ToNot(HaveOccurred())
}
By("creating an secret stores gcp")
s.CreateSAKeyStore(s.framework.Namespace.Name)
s.CreatePodIDStore(s.framework.Namespace.Name)
s.CreateSpecifcSASecretStore(s.framework.Namespace.Name)
}
By("creating an secret store for vault")
secretStore := &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: s.framework.Namespace.Name,
Namespace: s.framework.Namespace.Name,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
GCPSM: &esv1alpha1.GCPSMProvider{
ProjectID: s.projectID,
Auth: esv1alpha1.GCPSMAuth{
SecretRef: esv1alpha1.GCPSMAuthSecretRef{
SecretAccessKey: esmeta.SecretKeySelector{
Name: "provider-secret",
Key: "secret-access-credentials",
},
},
},
},
func (s *GcpProvider) CreateSAKeyStore(ns string) {
secretStore := makeStore(s)
secretStore.Spec.Provider.GCPSM.Auth = esv1alpha1.GCPSMAuth{
SecretRef: &esv1alpha1.GCPSMAuthSecretRef{
SecretAccessKey: esmeta.SecretKeySelector{
Name: "provider-secret",
Key: "secret-access-credentials",
},
},
}
err = s.framework.CRClient.Create(context.Background(), secretStore)
err := s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s *GcpProvider) CreatePodIDStore(ns string) {
secretStore := makeStore(s)
secretStore.ObjectMeta.Name = PodIDSecretStoreName
err := s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s *GcpProvider) CreateSpecifcSASecretStore(ns string) {
clusterSecretStore := makeCStore(s)
clusterSecretStore.ObjectMeta.Name = SpecifcSASecretStoreName
clusterSecretStore.Spec.Provider.GCPSM.Auth = esv1alpha1.GCPSMAuth{
WorkloadIdentity: &esv1alpha1.GCPWorkloadIdentity{
ClusterLocation: s.clusterLocation,
ClusterName: s.clusterName,
ServiceAccountRef: esmeta.ServiceAccountSelector{
Name: s.serviceAccountName,
Namespace: utilpointer.StringPtr(s.serviceAccountNamespace),
},
},
}
var cSS esv1alpha1.ClusterSecretStore
err := s.framework.CRClient.Get(context.Background(), types.NamespacedName{
Name: SpecifcSASecretStoreName,
}, &cSS)
if apierrors.IsNotFound(err) {
err := s.framework.CRClient.Create(context.Background(), clusterSecretStore)
Expect(err).ToNot(HaveOccurred())
} else {
log.Logf("%s CSStore already created", SpecifcSASecretStoreName)
}
}

View file

@ -0,0 +1,86 @@
/*
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.
limitations under the License.
*/
package gcpmanaged
import (
"os"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/ginkgo/extensions/table"
// nolint
// . "github.com/onsi/gomega"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/e2e/framework"
"github.com/external-secrets/external-secrets/e2e/suite/common"
"github.com/external-secrets/external-secrets/e2e/suite/gcp"
)
const (
withPodID = "sync secrets with pod identity"
withSpecifcSA = "sync secrets with specificSA identity"
)
var _ = Describe("[gcpmanaged] ", func() {
if os.Getenv("FOCUS") == "gcpmanaged" {
f := framework.New("eso-gcp-managed")
projectID := os.Getenv("GCP_PROJECT_ID")
clusterLocation := "europe-west1-b"
clusterName := "test-cluster"
serviceAccountName := os.Getenv("GCP_KSA_NAME")
serviceAccountNamespace := "default"
prov := &gcp.GcpProvider{}
if projectID != "" {
prov = gcp.NewgcpProvider(f, "", projectID, clusterLocation, clusterName, serviceAccountName, serviceAccountNamespace)
}
DescribeTable("sync secrets",
framework.TableFunc(f,
prov),
// uses pod id
framework.Compose(withPodID, f, common.SimpleDataSync, usePodIDESReference),
framework.Compose(withPodID, f, common.JSONDataWithProperty, usePodIDESReference),
framework.Compose(withPodID, f, common.JSONDataFromSync, usePodIDESReference),
framework.Compose(withPodID, f, common.NestedJSONWithGJSON, usePodIDESReference),
framework.Compose(withPodID, f, common.JSONDataWithTemplate, usePodIDESReference),
framework.Compose(withPodID, f, common.DockerJSONConfig, usePodIDESReference),
framework.Compose(withPodID, f, common.DataPropertyDockerconfigJSON, usePodIDESReference),
framework.Compose(withPodID, f, common.SSHKeySync, usePodIDESReference),
framework.Compose(withPodID, f, common.SSHKeySyncDataProperty, usePodIDESReference),
framework.Compose(withPodID, f, common.SyncWithoutTargetName, usePodIDESReference),
framework.Compose(withPodID, f, common.JSONDataWithoutTargetName, usePodIDESReference),
// uses specific sa
framework.Compose(withSpecifcSA, f, common.JSONDataFromSync, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.JSONDataWithProperty, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.JSONDataFromSync, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.NestedJSONWithGJSON, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.JSONDataWithTemplate, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.DockerJSONConfig, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.DataPropertyDockerconfigJSON, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.SSHKeySync, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.SSHKeySyncDataProperty, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.SyncWithoutTargetName, useSpecifcSAESReference),
framework.Compose(withSpecifcSA, f, common.JSONDataWithoutTargetName, useSpecifcSAESReference),
)
}
})
func usePodIDESReference(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = gcp.PodIDSecretStoreName
}
func useSpecifcSAESReference(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Kind = esv1alpha1.ClusterSecretStoreKind
tc.ExternalSecret.Spec.SecretStoreRef.Name = gcp.SpecifcSASecretStoreName
}

View file

@ -19,5 +19,6 @@ import (
_ "github.com/external-secrets/external-secrets/e2e/suite/aws"
_ "github.com/external-secrets/external-secrets/e2e/suite/azure"
_ "github.com/external-secrets/external-secrets/e2e/suite/gcp"
_ "github.com/external-secrets/external-secrets/e2e/suite/gcpmanaged"
_ "github.com/external-secrets/external-secrets/e2e/suite/vault"
)

View file

@ -48,6 +48,10 @@ const (
kubernetesProviderName = "kubernetes-provider"
)
var (
secretStorePath = "secret"
)
func newVaultProvider(f *framework.Framework) *vaultProvider {
prov := &vaultProvider{
framework: f,
@ -104,7 +108,7 @@ func makeStore(name, ns string, v *addon.Vault) *esv1alpha1.SecretStore {
Provider: &esv1alpha1.SecretStoreProvider{
Vault: &esv1alpha1.VaultProvider{
Version: esv1alpha1.VaultKVStoreV2,
Path: "secret",
Path: &secretStorePath,
Server: v.VaultURL,
CABundle: v.VaultServerCA,
},
@ -215,8 +219,9 @@ func (s vaultProvider) CreateV1Store(v *addon.Vault, ns string) {
err := s.framework.CRClient.Create(context.Background(), vaultCreds)
Expect(err).ToNot(HaveOccurred())
secretStore := makeStore(kvv1ProviderName, ns, v)
secretV1StorePath := "secret_v1"
secretStore.Spec.Provider.Vault.Version = esv1alpha1.VaultKVStoreV1
secretStore.Spec.Provider.Vault.Path = "secret_v1"
secretStore.Spec.Provider.Vault.Path = &secretV1StorePath
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
TokenSecretRef: &esmeta.SecretKeySelector{
Name: "v1-provider",
@ -242,6 +247,7 @@ func (s vaultProvider) CreateJWTStore(v *addon.Vault, ns string) {
secretStore := makeStore(jwtProviderName, ns, v)
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
Jwt: &esv1alpha1.VaultJwtAuth{
Path: v.JWTPath,
Role: v.JWTRole,
SecretRef: esmeta.SecretKeySelector{
Name: "jwt-provider",

View file

@ -39,58 +39,44 @@ var _ = Describe("[vault] ", func() {
framework.TableFunc(f,
newVaultProvider(f)),
// uses token auth
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),
framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth),
framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth),
framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
// use cert auth
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),
framework.Compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth),
framework.Compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth),
framework.Compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth),
framework.Compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth),
framework.Compose(withCertAuth, f, common.JSONDataWithoutTargetName, useCertAuth),
// use approle auth
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),
framework.Compose(withApprole, f, common.JSONDataFromSync, useApproleAuth),
framework.Compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth),
framework.Compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth),
framework.Compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth),
framework.Compose(withApprole, f, common.JSONDataWithoutTargetName, useApproleAuth),
// use v1 provider
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),
framework.Compose(withV1, f, common.JSONDataFromSync, useV1Provider),
framework.Compose(withV1, f, common.JSONDataWithProperty, useV1Provider),
framework.Compose(withV1, f, common.JSONDataWithTemplate, useV1Provider),
framework.Compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider),
framework.Compose(withV1, f, common.JSONDataWithoutTargetName, useV1Provider),
// use jwt provider
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),
framework.Compose(withJWT, f, common.JSONDataFromSync, useJWTProvider),
framework.Compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider),
framework.Compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider),
framework.Compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider),
framework.Compose(withJWT, f, common.JSONDataWithoutTargetName, useJWTProvider),
// use kubernetes provider
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),
framework.Compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider),
framework.Compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider),
framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
framework.Compose(withK8s, f, common.JSONDataWithoutTargetName, useKubernetesProvider),
)
})
func compose(descAppend string, f *framework.Framework, fn func(f *framework.Framework) (string, func(*framework.TestCase)), tweaks ...func(*framework.TestCase)) TableEntry {
desc, tfn := fn(f)
tweaks = append(tweaks, tfn)
te := Entry(desc + " " + descAppend)
// need to convert []func to []interface{}
ifs := make([]interface{}, len(tweaks))
for i := 0; i < len(tweaks); i++ {
ifs[i] = tweaks[i]
}
te.Parameters = ifs
return te
}
func useTokenAuth(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
}

247
go.mod
View file

@ -1,93 +1,214 @@
module github.com/external-secrets/external-secrets
go 1.16
go 1.17
replace (
github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1
github.com/external-secrets/external-secrets/e2e/framework/log => ./e2e/framework/log
github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
google.golang.org/grpc => google.golang.org/grpc v1.27.0
k8s.io/api => k8s.io/api v0.21.2
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.2
k8s.io/apimachinery => k8s.io/apimachinery v0.21.2
k8s.io/apiserver => k8s.io/apiserver v0.21.2
k8s.io/cli-runtime => k8s.io/cli-runtime v0.21.2
k8s.io/client-go => k8s.io/client-go v0.21.2
k8s.io/cloud-provider => k8s.io/cloud-provider v0.21.2
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.21.2
k8s.io/code-generator => k8s.io/code-generator v0.21.2
k8s.io/component-base => k8s.io/component-base v0.21.2
k8s.io/component-helpers => k8s.io/component-helpers v0.21.2
k8s.io/controller-manager => k8s.io/controller-manager v0.21.2
k8s.io/cri-api => k8s.io/cri-api v0.21.2
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.21.2
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.21.2
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.21.2
k8s.io/kube-proxy => k8s.io/kube-proxy v0.21.2
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.21.2
k8s.io/kubectl => k8s.io/kubectl v0.21.2
k8s.io/kubelet => k8s.io/kubelet v0.21.2
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.21.2
k8s.io/metrics => k8s.io/metrics v0.21.2
k8s.io/mount-utils => k8s.io/mount-utils v0.21.2
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.21.2
k8s.io/api => k8s.io/api v0.23.0
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.0
k8s.io/apimachinery => k8s.io/apimachinery v0.23.0
k8s.io/apiserver => k8s.io/apiserver v0.23.0
k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.0
k8s.io/client-go => k8s.io/client-go v0.23.0
k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.0
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.0
k8s.io/code-generator => k8s.io/code-generator v0.23.0
k8s.io/component-base => k8s.io/component-base v0.23.0
k8s.io/component-helpers => k8s.io/component-helpers v0.23.0
k8s.io/controller-manager => k8s.io/controller-manager v0.23.0
k8s.io/cri-api => k8s.io/cri-api v0.23.0
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.0
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.0
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.0
k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.0
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.0
k8s.io/kubectl => k8s.io/kubectl v0.23.0
k8s.io/kubelet => k8s.io/kubelet v0.23.0
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.0
k8s.io/metrics => k8s.io/metrics v0.23.0
k8s.io/mount-utils => k8s.io/mount-utils v0.23.0
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.0
)
require (
cloud.google.com/go v0.81.0
github.com/Azure/azure-sdk-for-go v54.1.0+incompatible
cloud.google.com/go v0.99.0
cloud.google.com/go/secretmanager v1.0.0
github.com/Azure/azure-sdk-for-go v61.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/IBM/go-sdk-core/v5 v5.8.0
github.com/IBM/secrets-manager-go-sdk v1.0.31
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/PaesslerAG/jsonpath v0.1.1
github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2
github.com/akeylesslabs/akeyless-go/v2 v2.5.11
github.com/akeylesslabs/akeyless-go/v2 v2.15.24
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/v4 v4.1.0
github.com/google/go-cmp v0.5.5
github.com/google/gofuzz v1.2.0 // indirect
github.com/crossplane/crossplane-runtime v0.15.1
github.com/go-logr/logr v1.2.2
github.com/golang-jwt/jwt/v4 v4.2.0
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.2.0
github.com/googleapis/gax-go v1.0.3
github.com/hashicorp/go-hclog v0.14.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.1-vault // indirect
github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4
github.com/huandu/xstrings v1.3.2 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/lestrrat-go/jwx v1.2.1
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.16.0
github.com/onsi/gomega v1.17.0
github.com/oracle/oci-go-sdk/v45 v45.2.0
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/prometheus/client_golang v1.11.0
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
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
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-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
k8s.io/client-go v0.21.2
k8s.io/utils v0.0.0-20210527160623-6fdb442a123b
sigs.k8s.io/controller-runtime v0.9.3
go.uber.org/zap v1.20.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
google.golang.org/api v0.61.0
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0
google.golang.org/grpc v1.43.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
k8s.io/api v0.23.0
k8s.io/apimachinery v0.23.0
k8s.io/client-go v0.23.0
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b
sigs.k8s.io/controller-runtime v0.11.0
sigs.k8s.io/controller-tools v0.5.0
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)
require (
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/PaesslerAG/gval v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/aws/aws-sdk-go-v2 v0.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 // indirect
github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
github.com/frankban/quicktest v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/go-openapi/errors v0.19.8 // indirect
github.com/go-openapi/strfmt v0.20.2 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/gobuffalo/flect v0.2.2 // indirect
github.com/goccy/go-json v0.4.8 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v0.14.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.1-vault // indirect
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.0 // indirect
github.com/lestrrat-go/iter v1.0.1 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
go.mongodb.org/mongo-driver v1.5.1 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.1.4 // indirect
k8s.io/apiextensions-apiserver v0.23.0 // indirect
k8s.io/component-base v0.23.0 // indirect
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect
k8s.io/klog v0.3.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

501
go.sum

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,8 @@ nav:
- Common K8S Secret Types: guides-common-k8s-secret-types.md
- Multi Tenancy: guides-multi-tenancy.md
- Metrics: guides-metrics.md
- Using Latest Image: guides-using-latest-image.md
- GitOps using FluxCD: guides-gitops-using-fluxcd.md
- Provider:
- AWS:
- Secrets Manager: provider-aws-secrets-manager.md
@ -53,6 +55,7 @@ nav:
- Gitlab Project Variables: provider-gitlab-project-variables.md
- Oracle:
- Oracle Vault: provider-oracle-vault.md
- Webhook: provider-webhook.md
- References:
- API specification: spec.md
- Contributing:

View file

@ -24,6 +24,7 @@ import (
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@ -45,6 +46,7 @@ func main() {
var metricsAddr string
var controllerClass string
var enableLeaderElection bool
var concurrent int
var loglevel string
var namespace string
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
@ -52,6 +54,7 @@ func main() {
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.IntVar(&concurrent, "concurrent", 1, "The number of concurrent ExternalSecret reconciles.")
flag.StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
flag.StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only")
flag.Parse()
@ -93,7 +96,9 @@ func main() {
Scheme: mgr.GetScheme(),
ControllerClass: controllerClass,
RequeueInterval: time.Hour,
}).SetupWithManager(mgr); err != nil {
}).SetupWithManager(mgr, controller.Options{
MaxConcurrentReconciles: concurrent,
}); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ExternalSecret")
os.Exit(1)
}

View file

@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@ -278,10 +279,9 @@ func patchSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, s
if !unversioned && len(gvks) == 1 {
secret.SetGroupVersionKind(gvks[0])
}
// we might get into a conflict here if we are not the manager of that particular field
// we do not resolve the conflict and return an error instead
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
err = c.Patch(ctx, secret, client.Apply, client.FieldOwner("external-secrets"))
// we're not able to resolve conflicts so we force ownership
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#using-server-side-apply-in-a-controller
err = c.Patch(ctx, secret, client.Apply, client.FieldOwner("external-secrets"), client.ForceOwnership)
if err != nil {
return fmt.Errorf(errPolicyMergePatch, secret.Name, err)
}
@ -418,8 +418,9 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient p
}
// SetupWithManager returns a new controller builder that will be started by the provided Manager.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
return ctrl.NewControllerManagedBy(mgr).
WithOptions(opts).
For(&esv1alpha1.ExternalSecret{}).
Owns(&v1.Secret{}).
Complete(r)

View file

@ -341,8 +341,7 @@ var _ = Describe("ExternalSecret controller", func() {
}
}
// controller should not force override but
// return an error on conflict
// controller should force ownership
mergeWithConflict := func(tc *testCase) {
const secretVal = "someValue"
// this should confict
@ -362,23 +361,14 @@ var _ = Describe("ExternalSecret controller", func() {
}, client.FieldOwner(FakeManager))).To(Succeed())
fakeProvider.WithGetSecret([]byte(secretVal), nil)
tc.checkCondition = func(es *esv1alpha1.ExternalSecret) bool {
cond := GetExternalSecretCondition(es.Status, esv1alpha1.ExternalSecretReady)
if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1alpha1.ConditionReasonSecretSyncedError {
return false
}
return true
}
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
// check that value stays the same
Expect(string(secret.Data[existingKey])).To(Equal(existingVal))
Expect(string(secret.Data[targetProp])).ToNot(Equal(secretVal))
Expect(string(secret.Data[existingKey])).To(Equal(secretVal))
// check owner/managedFields
Expect(hasOwnerRef(secret.ObjectMeta, "ExternalSecret", ExternalSecretName)).To(BeFalse())
Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(1))
Expect(hasFieldOwnership(secret.ObjectMeta, FakeManager, "{\"f:data\":{\".\":{},\"f:targetProperty\":{}},\"f:type\":{}}")).To(BeTrue())
Expect(secret.ObjectMeta.ManagedFields).To(HaveLen(2))
Expect(hasFieldOwnership(secret.ObjectMeta, "external-secrets", "{\"f:data\":{\"f:targetProperty\":{}},\"f:immutable\":{},\"f:metadata\":{\"f:annotations\":{\"f:reconcile.external-secrets.io/data-hash\":{}}}}")).To(BeTrue())
}
}

View file

@ -26,6 +26,7 @@ import (
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@ -79,7 +80,9 @@ var _ = BeforeSuite(func() {
Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
RequeueInterval: time.Second,
}).SetupWithManager(k8sManager)
}).SetupWithManager(k8sManager, controller.Options{
MaxConcurrentReconciles: 1,
})
Expect(err).ToNot(HaveOccurred())
go func() {

View file

@ -111,14 +111,20 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
if err != nil {
return nil, err
}
kv := make(map[string]string)
kv := make(map[string]json.RawMessage)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
}
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
var strVal string
err = json.Unmarshal(v, &strVal)
if err == nil {
secretData[k] = []byte(strVal)
} else {
secretData[k] = v
}
}
return secretData, nil
}

View file

@ -243,6 +243,12 @@ func TestGetSecretMap(t *testing.T) {
smtc.expectedData["foo"] = []byte("bar")
}
// good case: nested json
setNestedJSON := func(smtc *secretsManagerTestCase) {
smtc.apiOutput.SecretString = aws.String(`{"foobar":{"baz":"nestedval"}}`)
smtc.expectedData["foobar"] = []byte("{\"baz\":\"nestedval\"}")
}
// good case: caching
cachedMap := func(smtc *secretsManagerTestCase) {
smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "plus": "one"}`)
@ -259,6 +265,7 @@ func TestGetSecretMap(t *testing.T) {
successCases := []*secretsManagerTestCase{
makeValidSecretsManagerTestCaseCustom(setDeserialization),
makeValidSecretsManagerTestCaseCustom(setNestedJSON),
makeValidSecretsManagerTestCaseCustom(setAPIErr),
makeValidSecretsManagerTestCaseCustom(setInvalidJSON),
makeValidSecretsManagerTestCaseCustom(cachedMap),

View file

@ -17,188 +17,51 @@ import (
"context"
"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
"github.com/google/uuid"
mock "github.com/stretchr/testify/mock"
)
type secretData struct {
item keyvault.SecretItem
secretVersions map[string]keyvault.SecretBundle
lastVersion string
type AzureMockClient struct {
getKey func(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string) (result keyvault.KeyBundle, err error)
getSecret func(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
getSecretsComplete func(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error)
getCertificate func(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
}
type keyData struct {
item keyvault.KeyItem
keyVersions map[string]keyvault.KeyBundle
lastVersion string
func (mc *AzureMockClient) GetSecret(ctx context.Context, vaultBaseURL, secretName, secretVersion string) (result keyvault.SecretBundle, err error) {
return mc.getSecret(ctx, vaultBaseURL, secretName, secretVersion)
}
type AzureMock struct {
mock.Mock
knownSecrets map[string]map[string]*secretData
knownKeys map[string]map[string]*keyData
func (mc *AzureMockClient) GetCertificate(ctx context.Context, vaultBaseURL, certificateName, certificateVersion string) (result keyvault.CertificateBundle, err error) {
return mc.getCertificate(ctx, vaultBaseURL, certificateName, certificateVersion)
}
func (m *AzureMock) AddSecret(vaultBaseURL, secretName, secretContent string, enabled bool) string {
uid := uuid.NewString()
m.AddSecretWithVersion(vaultBaseURL, secretName, uid, secretContent, enabled)
return uid
func (mc *AzureMockClient) GetKey(ctx context.Context, vaultBaseURL, keyName, keyVersion string) (result keyvault.KeyBundle, err error) {
return mc.getKey(ctx, vaultBaseURL, keyName, keyVersion)
}
func (m *AzureMock) AddSecretWithVersion(vaultBaseURL, secretName, secretVersion, secretContent string, enabled bool) {
if m.knownSecrets == nil {
m.knownSecrets = make(map[string]map[string]*secretData)
}
if m.knownSecrets[vaultBaseURL] == nil {
m.knownSecrets[vaultBaseURL] = make(map[string]*secretData)
}
func (mc *AzureMockClient) GetSecretsComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error) {
return mc.getSecretsComplete(ctx, vaultBaseURL, maxresults)
}
secretItemID := vaultBaseURL + secretName
secretBundleID := secretItemID + "/" + secretVersion
if m.knownSecrets[vaultBaseURL][secretName] == nil {
m.knownSecrets[vaultBaseURL][secretName] = &secretData{
item: newValidSecretItem(secretItemID, enabled),
secretVersions: make(map[string]keyvault.SecretBundle),
func (mc *AzureMockClient) WithValue(serviceURL, secretName, secretVersion string, apiOutput keyvault.SecretBundle, err error) {
if mc != nil {
mc.getSecret = func(ctx context.Context, serviceURL, secretName, secretVersion string) (result keyvault.SecretBundle, retErr error) {
return apiOutput, err
}
} else {
m.knownSecrets[vaultBaseURL][secretName].item.Attributes.Enabled = &enabled
}
m.knownSecrets[vaultBaseURL][secretName].secretVersions[secretVersion] = newValidSecretBundle(secretBundleID, secretContent)
m.knownSecrets[vaultBaseURL][secretName].lastVersion = secretVersion
}
func newValidSecretBundle(secretBundleID, secretContent string) keyvault.SecretBundle {
return keyvault.SecretBundle{
Value: &secretContent,
ID: &secretBundleID,
}
}
func newValidSecretItem(secretItemID string, enabled bool) keyvault.SecretItem {
return keyvault.SecretItem{
ID: &secretItemID,
Attributes: &keyvault.SecretAttributes{Enabled: &enabled},
}
}
func (m *AzureMock) ExpectsGetSecret(ctx context.Context, vaultBaseURL, secretName, secretVersion string) {
data := m.knownSecrets[vaultBaseURL][secretName]
version := secretVersion
if version == "" {
version = data.lastVersion
}
returnValue := data.secretVersions[version]
m.On("GetSecret", ctx, vaultBaseURL, secretName, secretVersion).Return(returnValue, nil)
}
func (m *AzureMock) ExpectsGetSecretsComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) {
secretMap := m.knownSecrets[vaultBaseURL]
secretItems := make([]keyvault.SecretItem, len(secretMap))
i := 0
for _, value := range secretMap {
secretItems[i] = value.item
i++
}
firstPage := keyvault.SecretListResult{
Value: &secretItems,
NextLink: nil,
}
returnValue := keyvault.NewSecretListResultIterator(keyvault.NewSecretListResultPage(firstPage, func(context.Context, keyvault.SecretListResult) (keyvault.SecretListResult, error) {
return keyvault.SecretListResult{}, nil
}))
m.On("GetSecretsComplete", ctx, vaultBaseURL, maxresults).Return(returnValue, nil)
}
func (m *AzureMock) AddKey(vaultBaseURL, keyName string, key *keyvault.JSONWebKey, enabled bool) string {
uid := uuid.NewString()
m.AddKeyWithVersion(vaultBaseURL, keyName, uid, key, enabled)
return uid
}
func (m *AzureMock) AddKeyWithVersion(vaultBaseURL, keyName, keyVersion string, key *keyvault.JSONWebKey, enabled bool) {
if m.knownKeys == nil {
m.knownKeys = make(map[string]map[string]*keyData)
}
if m.knownKeys[vaultBaseURL] == nil {
m.knownKeys[vaultBaseURL] = make(map[string]*keyData)
}
keyItemID := vaultBaseURL + keyName
if m.knownKeys[vaultBaseURL][keyName] == nil {
m.knownKeys[vaultBaseURL][keyName] = &keyData{
item: newValidKeyItem(keyItemID, enabled),
keyVersions: make(map[string]keyvault.KeyBundle),
func (mc *AzureMockClient) WithKey(serviceURL, secretName, secretVersion string, apiOutput keyvault.KeyBundle, err error) {
if mc != nil {
mc.getKey = func(ctx context.Context, vaultBaseURL, keyName, keyVersion string) (result keyvault.KeyBundle, retErr error) {
return apiOutput, err
}
} else {
m.knownKeys[vaultBaseURL][keyName].item.Attributes.Enabled = &enabled
}
m.knownKeys[vaultBaseURL][keyName].keyVersions[keyVersion] = newValidKeyBundle(key)
m.knownKeys[vaultBaseURL][keyName].lastVersion = keyVersion
}
func newValidKeyBundle(key *keyvault.JSONWebKey) keyvault.KeyBundle {
return keyvault.KeyBundle{
Key: key,
}
}
func newValidKeyItem(keyItemID string, enabled bool) keyvault.KeyItem {
return keyvault.KeyItem{
Kid: &keyItemID,
Attributes: &keyvault.KeyAttributes{Enabled: &enabled},
func (mc *AzureMockClient) WithCertificate(serviceURL, secretName, secretVersion string, apiOutput keyvault.CertificateBundle, err error) {
if mc != nil {
mc.getCertificate = func(ctx context.Context, vaultBaseURL, keyName, keyVersion string) (result keyvault.CertificateBundle, retErr error) {
return apiOutput, err
}
}
}
func (m *AzureMock) ExpectsGetKey(ctx context.Context, vaultBaseURL, keyName, keyVersion string) {
data := m.knownKeys[vaultBaseURL][keyName]
version := keyVersion
if version == "" {
version = data.lastVersion
}
returnValue := data.keyVersions[version]
m.On("GetKey", ctx, vaultBaseURL, keyName, keyVersion).Return(returnValue, nil)
}
func (m *AzureMock) ExpectsGetKeysComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) {
keyMap := m.knownKeys[vaultBaseURL]
keyItems := make([]keyvault.KeyItem, len(keyMap))
i := 0
for _, value := range keyMap {
keyItems[i] = value.item
i++
}
firstPage := keyvault.KeyListResult{
Value: &keyItems,
NextLink: nil,
}
returnValue := keyvault.NewKeyListResultIterator(keyvault.NewKeyListResultPage(firstPage, func(context.Context, keyvault.KeyListResult) (keyvault.KeyListResult, error) {
return keyvault.KeyListResult{}, nil
}))
m.On("GetKeysComplete", ctx, vaultBaseURL, maxresults).Return(returnValue, nil)
}
func (m *AzureMock) GetKey(ctx context.Context, vaultBaseURL, keyName, keyVersion string) (result keyvault.KeyBundle, err error) {
args := m.Called(ctx, vaultBaseURL, keyName, keyVersion)
return args.Get(0).(keyvault.KeyBundle), args.Error(1)
}
func (m *AzureMock) GetSecret(ctx context.Context, vaultBaseURL, secretName, secretVersion string) (result keyvault.SecretBundle, err error) {
args := m.Called(ctx, vaultBaseURL, secretName, secretVersion)
return args.Get(0).(keyvault.SecretBundle), args.Error(1)
}
func (m *AzureMock) GetCertificate(ctx context.Context, vaultBaseURL, certificateName, certificateVersion string) (result keyvault.CertificateBundle, err error) {
args := m.Called(ctx, vaultBaseURL, certificateName, certificateVersion)
return args.Get(0).(keyvault.CertificateBundle), args.Error(1)
}
func (m *AzureMock) GetSecretsComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error) {
args := m.Called(ctx, vaultBaseURL, maxresults)
return args.Get(0).(keyvault.SecretListResultIterator), args.Error(1)
}
func (m *AzureMock) GetKeysComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.KeyListResultIterator, err error) {
args := m.Called(ctx, vaultBaseURL, maxresults)
return args.Get(0).(keyvault.KeyListResultIterator), args.Error(1)
}

View file

@ -36,11 +36,9 @@ import (
const (
defaultObjType = "secret"
vaultResource = "https://vault.azure.net"
)
// Provider satisfies the provider interface.
type Provider struct{}
// interface to keyvault.BaseClient.
type SecretClient interface {
GetKey(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string) (result keyvault.KeyBundle, err error)
@ -49,7 +47,6 @@ type SecretClient interface {
GetCertificate(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
}
// Azure satisfies the provider.SecretsClient interface.
type Azure struct {
kube client.Client
store esv1alpha1.GenericStore
@ -59,13 +56,13 @@ type Azure struct {
}
func init() {
schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
schema.Register(&Azure{}, &esv1alpha1.SecretStoreProvider{
AzureKV: &esv1alpha1.AzureKVProvider{},
})
}
// 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) {
func (a *Azure) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
return newClient(ctx, store, kube, namespace)
}
@ -75,15 +72,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.
@ -123,7 +123,7 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretData
}
return *secretResp.Cer, nil
case "key":
// returns a KeyBundla that contains a jwk
// returns a KeyBundle that contains a jwk
// azure kv returns only public keys
// see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#KeyBundle
keyResp, err := basicClient.GetKey(context.Background(), a.vaultURL, secretName, version)
@ -203,42 +203,75 @@ func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1alpha1.ExternalSecret
return secretsMap, nil
}
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) {

View file

@ -15,8 +15,10 @@ limitations under the License.
package keyvault
import (
context "context"
"context"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
@ -28,26 +30,98 @@ import (
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
fake "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault/fake"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
utils "github.com/external-secrets/external-secrets/pkg/utils"
)
func newAzure() (Azure, *fake.AzureMock) {
azureMock := &fake.AzureMock{}
testAzure := Azure{
baseClient: azureMock,
vaultURL: "https://local.vault/",
type secretManagerTestCase struct {
mockClient *fake.AzureMockClient
secretName string
secretVersion string
serviceURL string
ref *esv1alpha1.ExternalSecretDataRemoteRef
apiErr error
secretOutput keyvault.SecretBundle
keyOutput keyvault.KeyBundle
certOutput keyvault.CertificateBundle
expectError string
expectedSecret string
// for testing secretmap
expectedData map[string][]byte
}
func makeValidSecretManagerTestCase() *secretManagerTestCase {
secretString := "Hello World!"
smtc := secretManagerTestCase{
mockClient: &fake.AzureMockClient{},
secretName: "MySecret",
secretVersion: "",
ref: makeValidRef(),
secretOutput: keyvault.SecretBundle{Value: &secretString},
serviceURL: "",
apiErr: nil,
expectError: "",
expectedSecret: secretString,
expectedData: map[string][]byte{},
}
smtc.mockClient.WithValue(smtc.serviceURL, smtc.secretName, smtc.secretVersion, smtc.secretOutput, smtc.apiErr)
return &smtc
}
func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
smtc := makeValidSecretManagerTestCase()
for _, fn := range tweaks {
fn(smtc)
}
smtc.mockClient.WithValue(smtc.serviceURL, smtc.secretName, smtc.secretVersion, smtc.secretOutput, smtc.apiErr)
smtc.mockClient.WithKey(smtc.serviceURL, smtc.secretName, smtc.secretVersion, smtc.keyOutput, smtc.apiErr)
smtc.mockClient.WithCertificate(smtc.serviceURL, smtc.secretName, smtc.secretVersion, smtc.certOutput, smtc.apiErr)
return smtc
}
func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
namespace := "internal"
vaultURL := "https://local.vault.url"
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)
}
return testAzure, azureMock
}
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,127 +129,38 @@ 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 (
jwkPubRSA = `{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}`
jwkPubEC = `{"kid":"https://example.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}`
jwkPubRSA = `{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}`
jwkPubEC = `{"kid":"https://example.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}`
jsonTestString = `{"Name": "External", "LastName": "Secret", "Address": { "Street": "Myroad st.", "CP": "J4K4T4" } }`
jsonSingleTestString = `{"Name": "External", "LastName": "Secret" }`
keyName = "key/keyname"
certName = "cert/certname"
)
func TestGetKey(t *testing.T) {
testAzure, azureMock := newAzure()
ctx := context.Background()
tbl := []struct {
name string
kvName string
jwk *keyvault.JSONWebKey
out string
}{
{
name: "test public rsa key",
kvName: "my-rsa",
jwk: newKVJWK([]byte(jwkPubRSA)),
out: jwkPubRSA,
},
{
name: "test public ec key",
kvName: "my-ec",
jwk: newKVJWK([]byte(jwkPubEC)),
out: jwkPubEC,
},
}
for _, row := range tbl {
t.Run(row.name, func(t *testing.T) {
azureMock.AddKey(testAzure.vaultURL, row.kvName, row.jwk, true)
azureMock.ExpectsGetKey(ctx, testAzure.vaultURL, row.kvName, "")
rf := esv1alpha1.ExternalSecretDataRemoteRef{
Key: "key/" + row.kvName,
}
secret, err := testAzure.GetSecret(ctx, rf)
azureMock.AssertExpectations(t)
tassert.Nil(t, err, "the return err should be nil")
tassert.Equal(t, []byte(row.out), secret)
})
}
}
func TestGetSecretWithVersion(t *testing.T) {
testAzure, azureMock := newAzure()
ctx := context.Background()
version := "v1"
rf := esv1alpha1.ExternalSecretDataRemoteRef{
Key: "testName",
Version: version,
}
azureMock.AddSecretWithVersion(testAzure.vaultURL, "testName", version, "My Secret", true)
azureMock.ExpectsGetSecret(ctx, testAzure.vaultURL, "testName", version)
secret, err := testAzure.GetSecret(ctx, rf)
azureMock.AssertExpectations(t)
tassert.Nil(t, err, "the return err should be nil")
tassert.Equal(t, []byte("My Secret"), secret)
}
func TestGetSecretWithoutVersion(t *testing.T) {
testAzure, azureMock := newAzure()
ctx := context.Background()
rf := esv1alpha1.ExternalSecretDataRemoteRef{
Key: "testName",
}
azureMock.AddSecret(testAzure.vaultURL, "testName", "My Secret", true)
azureMock.ExpectsGetSecret(ctx, testAzure.vaultURL, "testName", "")
secret, err := testAzure.GetSecret(ctx, rf)
azureMock.AssertExpectations(t)
tassert.Nil(t, err, "the return err should be nil")
tassert.Equal(t, []byte("My Secret"), secret)
}
func TestGetSecretMap(t *testing.T) {
testAzure, azureMock := newAzure()
ctx := context.Background()
rf := esv1alpha1.ExternalSecretDataRemoteRef{
Key: "testName",
}
azureMock.AddSecret(testAzure.vaultURL, "testName", "{\"username\": \"user1\", \"pass\": \"123\"}", true)
azureMock.ExpectsGetSecret(ctx, testAzure.vaultURL, "testName", "")
secretMap, err := testAzure.GetSecretMap(ctx, rf)
azureMock.AssertExpectations(t)
tassert.Nil(t, err, "the return err should be nil")
tassert.Equal(t, secretMap, map[string][]byte{"username": []byte("user1"), "pass": []byte("123")})
}
func newKVJWK(b []byte) *keyvault.JSONWebKey {
var key keyvault.JSONWebKey
err := json.Unmarshal(b, &key)
@ -184,3 +169,210 @@ func newKVJWK(b []byte) *keyvault.JSONWebKey {
}
return &key
}
// test the sm<->azurekv interface
// make sure correct values are passed and errors are handled accordingly.
func TestAzureKeyVaultSecretManagerGetSecret(t *testing.T) {
secretString := "changedvalue"
secretCertificate := "certificate_value"
// good case
setSecretString := func(smtc *secretManagerTestCase) {
smtc.expectedSecret = secretString
smtc.secretOutput = keyvault.SecretBundle{
Value: &secretString,
}
}
setSecretStringWithVersion := func(smtc *secretManagerTestCase) {
smtc.expectedSecret = secretString
smtc.secretOutput = keyvault.SecretBundle{
Value: &secretString,
}
smtc.ref.Version = "v1"
smtc.secretVersion = smtc.ref.Version
}
setSecretWithProperty := func(smtc *secretManagerTestCase) {
jsonString := jsonTestString
smtc.expectedSecret = "External"
smtc.secretOutput = keyvault.SecretBundle{
Value: &jsonString,
}
smtc.ref.Property = "Name"
}
badSecretWithProperty := func(smtc *secretManagerTestCase) {
jsonString := jsonTestString
smtc.expectedSecret = ""
smtc.secretOutput = keyvault.SecretBundle{
Value: &jsonString,
}
smtc.ref.Property = "Age"
smtc.expectError = fmt.Sprintf("property %s does not exist in key %s", smtc.ref.Property, smtc.ref.Key)
smtc.apiErr = fmt.Errorf(smtc.expectError)
}
// // good case: key set
setPubRSAKey := func(smtc *secretManagerTestCase) {
smtc.secretName = keyName
smtc.expectedSecret = jwkPubRSA
smtc.keyOutput = keyvault.KeyBundle{
Key: newKVJWK([]byte(jwkPubRSA)),
}
smtc.ref.Key = smtc.secretName
}
// // good case: key set
setPubECKey := func(smtc *secretManagerTestCase) {
smtc.secretName = keyName
smtc.expectedSecret = jwkPubEC
smtc.keyOutput = keyvault.KeyBundle{
Key: newKVJWK([]byte(jwkPubEC)),
}
smtc.ref.Key = smtc.secretName
}
// // good case: key set
setCertificate := func(smtc *secretManagerTestCase) {
byteArrString := []byte(secretCertificate)
smtc.secretName = certName
smtc.expectedSecret = secretCertificate
smtc.certOutput = keyvault.CertificateBundle{
Cer: &byteArrString,
}
smtc.ref.Key = smtc.secretName
}
badSecretType := func(smtc *secretManagerTestCase) {
smtc.secretName = "name"
smtc.expectedSecret = ""
smtc.expectError = fmt.Sprintf("unknown Azure Keyvault object Type for %s", smtc.secretName)
smtc.ref.Key = fmt.Sprintf("dummy/%s", smtc.secretName)
}
successCases := []*secretManagerTestCase{
makeValidSecretManagerTestCase(),
makeValidSecretManagerTestCaseCustom(setSecretString),
makeValidSecretManagerTestCaseCustom(setSecretStringWithVersion),
makeValidSecretManagerTestCaseCustom(setSecretWithProperty),
makeValidSecretManagerTestCaseCustom(badSecretWithProperty),
makeValidSecretManagerTestCaseCustom(setPubRSAKey),
makeValidSecretManagerTestCaseCustom(setPubECKey),
makeValidSecretManagerTestCaseCustom(setCertificate),
makeValidSecretManagerTestCaseCustom(badSecretType),
}
sm := Azure{}
for k, v := range successCases {
sm.baseClient = v.mockClient
out, err := sm.GetSecret(context.Background(), *v.ref)
if !utils.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 TestAzureKeyVaultSecretManagerGetSecretMap(t *testing.T) {
secretString := "changedvalue"
secretCertificate := "certificate_value"
badSecretString := func(smtc *secretManagerTestCase) {
smtc.expectedSecret = secretString
smtc.secretOutput = keyvault.SecretBundle{
Value: &secretString,
}
smtc.expectError = "error unmarshalling json data: invalid character 'c' looking for beginning of value"
}
setSecretJSON := func(smtc *secretManagerTestCase) {
jsonString := jsonSingleTestString
smtc.secretOutput = keyvault.SecretBundle{
Value: &jsonString,
}
smtc.expectedData["Name"] = []byte("External")
smtc.expectedData["LastName"] = []byte("Secret")
}
setSecretJSONWithProperty := func(smtc *secretManagerTestCase) {
jsonString := jsonTestString
smtc.secretOutput = keyvault.SecretBundle{
Value: &jsonString,
}
smtc.ref.Property = "Address"
smtc.expectedData["Street"] = []byte("Myroad st.")
smtc.expectedData["CP"] = []byte("J4K4T4")
}
badSecretWithProperty := func(smtc *secretManagerTestCase) {
jsonString := jsonTestString
smtc.expectedSecret = ""
smtc.secretOutput = keyvault.SecretBundle{
Value: &jsonString,
}
smtc.ref.Property = "Age"
smtc.expectError = fmt.Sprintf("property %s does not exist in key %s", smtc.ref.Property, smtc.ref.Key)
smtc.apiErr = fmt.Errorf(smtc.expectError)
}
badPubRSAKey := func(smtc *secretManagerTestCase) {
smtc.secretName = keyName
smtc.expectedSecret = jwkPubRSA
smtc.keyOutput = keyvault.KeyBundle{
Key: newKVJWK([]byte(jwkPubRSA)),
}
smtc.ref.Key = smtc.secretName
smtc.expectError = "cannot get use dataFrom to get key secret"
}
badCertificate := func(smtc *secretManagerTestCase) {
byteArrString := []byte(secretCertificate)
smtc.secretName = certName
smtc.expectedSecret = secretCertificate
smtc.certOutput = keyvault.CertificateBundle{
Cer: &byteArrString,
}
smtc.ref.Key = smtc.secretName
smtc.expectError = "cannot get use dataFrom to get certificate secret"
}
badSecretType := func(smtc *secretManagerTestCase) {
smtc.secretName = "name"
smtc.expectedSecret = ""
smtc.expectError = fmt.Sprintf("unknown Azure Keyvault object Type for %s", smtc.secretName)
smtc.ref.Key = fmt.Sprintf("dummy/%s", smtc.secretName)
}
successCases := []*secretManagerTestCase{
makeValidSecretManagerTestCaseCustom(badSecretString),
makeValidSecretManagerTestCaseCustom(setSecretJSON),
makeValidSecretManagerTestCaseCustom(setSecretJSONWithProperty),
makeValidSecretManagerTestCaseCustom(badSecretWithProperty),
makeValidSecretManagerTestCaseCustom(badPubRSAKey),
makeValidSecretManagerTestCaseCustom(badCertificate),
makeValidSecretManagerTestCaseCustom(badSecretType),
}
sm := Azure{}
for k, v := range successCases {
sm.baseClient = v.mockClient
out, err := sm.GetSecretMap(context.Background(), *v.ref)
if !utils.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 makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
return &esv1alpha1.ExternalSecretDataRemoteRef{
Key: "test-secret",
Version: "default",
}
}

View file

@ -21,10 +21,11 @@ import (
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"github.com/googleapis/gax-go"
"github.com/tidwall/gjson"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
@ -40,11 +41,12 @@ const (
errGCPSMStore = "received invalid GCPSM SecretStore resource"
errClientClose = "unable to close SecretManager client: %w"
errMissingStoreSpec = "invalid: missing store spec"
errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
errInvalidClusterStoreMissingSANamespace = "invalid ClusterSecretStore: missing GCP Service Account Namespace"
errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
errMissingSAK = "missing SecretAccessKey"
errUnableProcessJSONCredentials = "failed to process the provided JSON credentials: %w"
errUnableProcessDefaultCredentials = "failed to process the default credentials: %w"
errUnableCreateGCPSMClient = "failed to create GCP secretmanager client: %w"
errUninitalizedGCPProvider = "provider GCP is not initialized"
errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w"
@ -63,43 +65,64 @@ type ProviderGCP struct {
}
type gClient struct {
kube kclient.Client
store *esv1alpha1.GCPSMProvider
namespace string
storeKind string
credentials []byte
kube kclient.Client
store *esv1alpha1.GCPSMProvider
namespace string
storeKind string
workloadIdentity *workloadIdentity
}
func (c *gClient) setAuth(ctx context.Context) error {
credentialsSecret := &corev1.Secret{}
credentialsSecretName := c.store.Auth.SecretRef.SecretAccessKey.Name
func (c *gClient) getTokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
ts, err := serviceAccountTokenSource(ctx, store, kube, namespace)
if ts != nil || err != nil {
return ts, err
}
ts, err = c.workloadIdentity.TokenSource(ctx, store, kube, namespace)
if ts != nil || err != nil {
return ts, err
}
return google.DefaultTokenSource(ctx, CloudPlatformRole)
}
func serviceAccountTokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
spec := store.GetSpec()
if spec == nil || spec.Provider.GCPSM == nil {
return nil, fmt.Errorf(errMissingStoreSpec)
}
sr := spec.Provider.GCPSM.Auth.SecretRef
if sr == nil {
return nil, nil
}
storeKind := store.GetObjectKind().GroupVersionKind().Kind
credentialsSecret := &v1.Secret{}
credentialsSecretName := sr.SecretAccessKey.Name
objectKey := types.NamespacedName{
Name: credentialsSecretName,
Namespace: c.namespace,
Namespace: namespace,
}
// only ClusterStore is allowed to set namespace (and then it's required)
if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
if credentialsSecretName != "" && c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
if storeKind == esv1alpha1.ClusterSecretStoreKind {
if credentialsSecretName != "" && sr.SecretAccessKey.Namespace == nil {
return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
} else if credentialsSecretName != "" {
objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
objectKey.Namespace = *sr.SecretAccessKey.Namespace
}
}
if credentialsSecretName == "" {
c.credentials = nil
return nil
}
err := c.kube.Get(ctx, objectKey, credentialsSecret)
err := kube.Get(ctx, objectKey, credentialsSecret)
if err != nil {
return fmt.Errorf(errFetchSAKSecret, err)
return nil, fmt.Errorf(errFetchSAKSecret, err)
}
c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.SecretAccessKey.Key]
if (c.credentials == nil) || (len(c.credentials) == 0) {
return fmt.Errorf(errMissingSAK)
credentials := credentialsSecret.Data[sr.SecretAccessKey.Key]
if (credentials == nil) || (len(credentials) == 0) {
return nil, fmt.Errorf(errMissingSAK)
}
return nil
config, err := google.JWTConfigFromJSON(credentials, CloudPlatformRole)
if err != nil {
return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
}
return config.TokenSource(ctx), nil
}
// NewClient constructs a GCP Provider.
@ -110,36 +133,26 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt
}
storeSpecGCPSM := storeSpec.Provider.GCPSM
cliStore := gClient{
kube: kube,
store: storeSpecGCPSM,
namespace: namespace,
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
wi, err := newWorkloadIdentity(ctx)
if err != nil {
return nil, fmt.Errorf("unable to initialize workload identity")
}
if err := cliStore.setAuth(ctx); err != nil {
return nil, err
cliStore := gClient{
kube: kube,
store: storeSpecGCPSM,
namespace: namespace,
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
workloadIdentity: wi,
}
sm.projectID = cliStore.store.ProjectID
if cliStore.credentials != nil {
config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
if err != nil {
return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
}
ts := config.TokenSource(ctx)
clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
if err != nil {
return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
}
sm.SecretManagerClient = clientGCPSM
return sm, nil
}
ts, err := google.DefaultTokenSource(ctx, CloudPlatformRole)
ts, err := cliStore.getTokenSource(ctx, store, kube, namespace)
if err != nil {
return nil, fmt.Errorf(errUnableProcessDefaultCredentials, err)
return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
}
clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
if err != nil {
return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
@ -204,7 +217,7 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
return nil, err
}
kv := make(map[string]string)
kv := make(map[string]json.RawMessage)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
@ -212,7 +225,13 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
var strVal string
err = json.Unmarshal(v, &strVal)
if err == nil {
secretData[k] = []byte(strVal)
} else {
secretData[k] = v
}
}
return secretData, nil

View file

@ -172,11 +172,19 @@ func TestGetSecretMap(t *testing.T) {
smtc.expectError = "unable to unmarshal secret"
}
// good case: deserialize nested json as []byte, if it's a string, decode the string
setNestedJSON := func(smtc *secretManagerTestCase) {
smtc.apiOutput.Payload.Data = []byte(`{"foo":{"bar":"baz"}, "qux": "qu\"z"}`)
smtc.expectedData["foo"] = []byte(`{"bar":"baz"}`)
smtc.expectedData["qux"] = []byte("qu\"z")
}
successCases := []*secretManagerTestCase{
makeValidSecretManagerTestCaseCustom(setDeserialization),
makeValidSecretManagerTestCaseCustom(setAPIErr),
makeValidSecretManagerTestCaseCustom(setNilMockClient),
makeValidSecretManagerTestCaseCustom(setInvalidJSON),
makeValidSecretManagerTestCaseCustom(setNestedJSON),
}
sm := ProviderGCP{}

View file

@ -0,0 +1,254 @@
/*
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 secretmanager
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
iam "cloud.google.com/go/iam/credentials/apiv1"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"github.com/googleapis/gax-go"
"golang.org/x/oauth2"
"google.golang.org/api/option"
credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpc.go4.org/credentials/oauth"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
)
const (
gcpSAAnnotation = "iam.gke.io/gcp-service-account"
errFetchPodToken = "unable to fetch pod token: %w"
errFetchIBToken = "unable to fetch identitybindingtoken: %w"
errGenAccessToken = "unable to generate gcp access token: %w"
)
// workloadIdentity holds all clients and generators needed
// to create a gcp oauth token.
type workloadIdentity struct {
iamClient IamClient
idBindTokenGenerator idBindTokenGenerator
saTokenGenerator saTokenGenerator
}
// interface to GCP IAM API.
type IamClient interface {
GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
}
// interface to securetoken/identitybindingtoken API.
type idBindTokenGenerator interface {
Generate(context.Context, *http.Client, string, string, string) (*oauth2.Token, error)
}
// interface to kubernetes serviceaccount token request API.
type saTokenGenerator interface {
Generate(context.Context, string, string, string) (*authenticationv1.TokenRequest, error)
}
func newWorkloadIdentity(ctx context.Context) (*workloadIdentity, error) {
iamc, err := newIAMClient(ctx)
if err != nil {
return nil, err
}
satg, err := newSATokenGenerator()
if err != nil {
return nil, err
}
return &workloadIdentity{
iamClient: iamc,
idBindTokenGenerator: newIDBindTokenGenerator(),
saTokenGenerator: satg,
}, nil
}
func (w *workloadIdentity) TokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
spec := store.GetSpec()
if spec == nil || spec.Provider == nil || spec.Provider.GCPSM == nil {
return nil, fmt.Errorf(errMissingStoreSpec)
}
wi := spec.Provider.GCPSM.Auth.WorkloadIdentity
if wi == nil {
return nil, nil
}
storeKind := store.GetObjectKind().GroupVersionKind().Kind
saKey := types.NamespacedName{
Name: wi.ServiceAccountRef.Name,
Namespace: namespace,
}
// only ClusterStore is allowed to set namespace (and then it's required)
if storeKind == esv1alpha1.ClusterSecretStoreKind {
if wi.ServiceAccountRef.Namespace == nil {
return nil, fmt.Errorf(errInvalidClusterStoreMissingSANamespace)
}
saKey.Namespace = *wi.ServiceAccountRef.Namespace
}
sa := &v1.ServiceAccount{}
err := kube.Get(ctx, saKey, sa)
if err != nil {
return nil, err
}
idProvider := fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s",
spec.Provider.GCPSM.ProjectID,
wi.ClusterLocation,
wi.ClusterName)
idPool := fmt.Sprintf("%s.svc.id.goog", spec.Provider.GCPSM.ProjectID)
gcpSA := sa.Annotations[gcpSAAnnotation]
resp, err := w.saTokenGenerator.Generate(ctx, idPool, saKey.Name, saKey.Namespace)
if err != nil {
return nil, fmt.Errorf(errFetchPodToken, err)
}
idBindToken, err := w.idBindTokenGenerator.Generate(ctx, http.DefaultClient, resp.Status.Token, idPool, idProvider)
if err != nil {
return nil, fmt.Errorf(errFetchIBToken, err)
}
// If no `iam.gke.io/gcp-service-account` annotation is present the
// identitybindingtoken will be used directly, allowing bindings on secrets
// of the form "serviceAccount:<project>.svc.id.goog[<namespace>/<sa>]".
if gcpSA == "" {
return oauth2.StaticTokenSource(idBindToken), nil
}
gcpSAResp, err := w.iamClient.GenerateAccessToken(ctx, &credentialspb.GenerateAccessTokenRequest{
Name: fmt.Sprintf("projects/-/serviceAccounts/%s", gcpSA),
Scope: secretmanager.DefaultAuthScopes(),
}, gax.WithGRPCOptions(grpc.PerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(idBindToken)})))
if err != nil {
return nil, fmt.Errorf(errGenAccessToken, err)
}
return oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: gcpSAResp.GetAccessToken(),
}), nil
}
func newIAMClient(ctx context.Context) (IamClient, error) {
iamOpts := []option.ClientOption{
option.WithUserAgent("external-secrets-operator"),
// tell the secretmanager library to not add transport-level ADC since
// we need to override on a per call basis
option.WithoutAuthentication(),
// grpc oauth TokenSource credentials require transport security, so
// this must be set explicitly even though TLS is used
option.WithGRPCDialOption(grpc.WithTransportCredentials(credentials.NewTLS(nil))),
option.WithGRPCConnectionPool(5),
}
return iam.NewIamCredentialsClient(ctx, iamOpts...)
}
type k8sSATokenGenerator struct {
corev1 clientcorev1.CoreV1Interface
}
func (g *k8sSATokenGenerator) Generate(ctx context.Context, idPool, name, namespace string) (*authenticationv1.TokenRequest, error) {
// Request a serviceaccount token for the pod
ttl := int64((15 * time.Minute).Seconds())
return g.corev1.
ServiceAccounts(namespace).
CreateToken(ctx, name,
&authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: &ttl,
Audiences: []string{idPool},
},
},
metav1.CreateOptions{},
)
}
func newSATokenGenerator() (saTokenGenerator, error) {
cfg, err := ctrlcfg.GetConfig()
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}
return &k8sSATokenGenerator{
corev1: clientset.CoreV1(),
}, nil
}
// Trades the kubernetes token for an identitybindingtoken token.
type gcpIDBindTokenGenerator struct {
targetURL string
}
func newIDBindTokenGenerator() idBindTokenGenerator {
return &gcpIDBindTokenGenerator{
targetURL: "https://securetoken.googleapis.com/v1/identitybindingtoken",
}
}
func (g *gcpIDBindTokenGenerator) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
body, err := json.Marshal(map[string]string{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token": k8sToken,
"audience": fmt.Sprintf("identitynamespace:%s:%s", idPool, idProvider),
"scope": "https://www.googleapis.com/auth/cloud-platform",
})
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", g.targetURL, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("could not get idbindtoken token, status: %v", resp.StatusCode)
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
idBindToken := &oauth2.Token{}
if err := json.Unmarshal(respBody, idBindToken); err != nil {
return nil, err
}
return idBindToken, nil
}

View file

@ -0,0 +1,392 @@
/*
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 secretmanager
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/googleapis/gax-go"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
authv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
type workloadIdentityTest struct {
name string
expTS bool
expToken *oauth2.Token
expErr string
genAccessToken func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
genIDBindToken func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
genSAToken func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error)
store esv1alpha1.GenericStore
kubeObjects []client.Object
}
func TestWorkloadIdentity(t *testing.T) {
clusterSANamespace := "foobar"
tbl := []*workloadIdentityTest{
composeTestcase(
defaultTestCase("missing store spec should result in error"),
withErr("invalid: missing store spec"),
withStore(&esv1alpha1.SecretStore{}),
),
composeTestcase(
defaultTestCase("should skip when no workload identity is configured: TokenSource and error must be nil"),
withStore(&esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
GCPSM: &esv1alpha1.GCPSMProvider{},
},
},
}),
),
composeTestcase(
defaultTestCase("return access token from GenerateAccessTokenRequest with SecretStore"),
withStore(defaultStore()),
expTokenSource(),
expectToken(defaultGenAccessToken),
),
composeTestcase(
defaultTestCase("return idBindToken when no annotation is set with SecretStore"),
expTokenSource(),
expectToken(defaultIDBindToken),
withStore(defaultStore()),
withK8sResources([]client.Object{
&v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
},
}),
),
composeTestcase(
defaultTestCase("invalid ClusterSecretStore: missing service account namespace"),
expErr("invalid ClusterSecretStore: missing GCP Service Account Namespace"),
withStore(
composeStore(defaultClusterStore()),
),
withK8sResources([]client.Object{
&v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
},
}),
),
composeTestcase(
defaultTestCase("return access token from GenerateAccessTokenRequest with ClusterSecretStore"),
expTokenSource(),
expectToken(defaultGenAccessToken),
withStore(
composeStore(defaultClusterStore(), withSANamespace(clusterSANamespace)),
),
withK8sResources([]client.Object{
&v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: clusterSANamespace,
Annotations: map[string]string{
gcpSAAnnotation: "example",
},
},
},
}),
),
}
for _, row := range tbl {
t.Run(row.name, func(t *testing.T) {
fakeIam := &fakeIAMClient{generateAccessTokenFunc: row.genAccessToken}
fakeIDBGen := &fakeIDBindTokenGen{generateFunc: row.genIDBindToken}
fakeSATG := &fakeSATokenGen{GenerateFunc: row.genSAToken}
w := &workloadIdentity{
iamClient: fakeIam,
idBindTokenGenerator: fakeIDBGen,
saTokenGenerator: fakeSATG,
}
cb := clientfake.NewClientBuilder()
cb.WithObjects(row.kubeObjects...)
client := cb.Build()
ts, err := w.TokenSource(context.Background(), row.store, client, "default")
// assert err
if row.expErr == "" {
assert.NoError(t, err)
} else {
assert.Error(t, err, row.expErr)
}
// assert ts
if row.expTS {
assert.NotNil(t, ts)
if row.expToken != nil {
tk, err := ts.Token()
assert.NoError(t, err)
assert.EqualValues(t, tk, row.expToken)
}
} else {
assert.Nil(t, ts)
}
})
}
}
func TestSATokenGen(t *testing.T) {
corev1 := &fakeK8sV1{}
g := &k8sSATokenGenerator{
corev1: corev1,
}
token, err := g.Generate(context.Background(), "my-fake-audience", "bar", "default")
assert.Nil(t, err)
assert.Equal(t, token.Status.Token, defaultSAToken)
assert.Equal(t, token.Spec.Audiences[0], "my-fake-audience")
}
func TestIDBTokenGen(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
payload := make(map[string]string)
rb, err := ioutil.ReadAll(r.Body)
assert.Nil(t, err)
err = json.Unmarshal(rb, &payload)
assert.Nil(t, err)
assert.Equal(t, payload["audience"], "identitynamespace:some-idpool:some-id-provider")
bt, err := json.Marshal(&oauth2.Token{
AccessToken: "12345",
})
assert.Nil(t, err)
rw.WriteHeader(http.StatusOK)
rw.Write(bt)
}))
defer srv.Close()
gen := &gcpIDBindTokenGenerator{
targetURL: srv.URL,
}
token, err := gen.Generate(context.Background(), http.DefaultClient, "some-token", "some-idpool", "some-id-provider")
assert.Nil(t, err)
assert.Equal(t, token.AccessToken, "12345")
}
type testCaseMutator func(tc *workloadIdentityTest)
func composeTestcase(tc *workloadIdentityTest, mutators ...testCaseMutator) *workloadIdentityTest {
for _, m := range mutators {
m(tc)
}
return tc
}
func withErr(err string) testCaseMutator {
return func(tc *workloadIdentityTest) {
tc.expErr = err
}
}
func withStore(store esv1alpha1.GenericStore) testCaseMutator {
return func(tc *workloadIdentityTest) {
tc.store = store
}
}
func expTokenSource() testCaseMutator {
return func(tc *workloadIdentityTest) {
tc.expTS = true
}
}
func expectToken(token string) testCaseMutator {
return func(tc *workloadIdentityTest) {
tc.expToken = &oauth2.Token{
AccessToken: token,
}
}
}
func expErr(err string) testCaseMutator {
return func(tc *workloadIdentityTest) {
tc.expErr = err
}
}
func withK8sResources(objs []client.Object) testCaseMutator {
return func(tc *workloadIdentityTest) {
tc.kubeObjects = objs
}
}
var (
defaultGenAccessToken = "default-gen-access-token"
defaultIDBindToken = "default-id-bind-token"
defaultSAToken = "default-k8s-sa-token"
)
func defaultTestCase(name string) *workloadIdentityTest {
return &workloadIdentityTest{
name: name,
genAccessToken: func(c context.Context, gatr *credentialspb.GenerateAccessTokenRequest, co ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
return &credentialspb.GenerateAccessTokenResponse{
AccessToken: defaultGenAccessToken,
}, nil
},
genIDBindToken: func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: defaultIDBindToken,
}, nil
},
genSAToken: func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error) {
return &authv1.TokenRequest{
Status: authv1.TokenRequestStatus{
Token: defaultSAToken,
},
}, nil
},
kubeObjects: []client.Object{
&v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{
gcpSAAnnotation: "example",
},
},
},
},
}
}
func defaultStore() *esv1alpha1.SecretStore {
return &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: "foobar",
Namespace: "default",
},
Spec: defaultStoreSpec(),
}
}
func defaultClusterStore() *esv1alpha1.ClusterSecretStore {
return &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
Kind: esv1alpha1.ClusterSecretStoreKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: "foobar",
},
Spec: defaultStoreSpec(),
}
}
func defaultStoreSpec() esv1alpha1.SecretStoreSpec {
return esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
GCPSM: &esv1alpha1.GCPSMProvider{
Auth: esv1alpha1.GCPSMAuth{
WorkloadIdentity: &esv1alpha1.GCPWorkloadIdentity{
ServiceAccountRef: esmeta.ServiceAccountSelector{
Name: "example",
},
ClusterLocation: "example",
ClusterName: "foobar",
},
},
ProjectID: "1234",
},
},
}
}
type storeMutator func(spc esv1alpha1.GenericStore)
func composeStore(store esv1alpha1.GenericStore, mutators ...storeMutator) esv1alpha1.GenericStore {
for _, m := range mutators {
m(store)
}
return store
}
func withSANamespace(namespace string) storeMutator {
return func(store esv1alpha1.GenericStore) {
spc := store.GetSpec()
spc.Provider.GCPSM.Auth.WorkloadIdentity.ServiceAccountRef.Namespace = &namespace
}
}
// fake IDBindToken Generator.
type fakeIDBindTokenGen struct {
generateFunc func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
}
func (g *fakeIDBindTokenGen) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
return g.generateFunc(ctx, client, k8sToken, idPool, idProvider)
}
// fake IAM Client.
type fakeIAMClient struct {
generateAccessTokenFunc func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
}
func (f *fakeIAMClient) GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
return f.generateAccessTokenFunc(ctx, req, opts...)
}
// fake SA Token Generator.
type fakeSATokenGen struct {
GenerateFunc func(context.Context, string, string, string) (*authv1.TokenRequest, error)
}
func (f *fakeSATokenGen) Generate(ctx context.Context, idPool, namespace, name string) (*authv1.TokenRequest, error) {
return f.GenerateFunc(ctx, idPool, namespace, name)
}
// fake k8s client for creating tokens.
type fakeK8sV1 struct {
k8sv1.CoreV1Interface
}
func (m *fakeK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
return &fakeK8sV1SA{v1mock: m}
}
// Mock the K8s service account client.
type fakeK8sV1SA struct {
k8sv1.ServiceAccountInterface
v1mock *fakeK8sV1
}
func (ma *fakeK8sV1SA) CreateToken(
ctx context.Context,
serviceAccountName string,
tokenRequest *authv1.TokenRequest,
opts metav1.CreateOptions,
) (*authv1.TokenRequest, error) {
tokenRequest.Status.Token = defaultSAToken
return tokenRequest, nil
}

View file

@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/IBM/go-sdk-core/v5/core"
sm "github.com/IBM/secrets-manager-go-sdk/secretsmanagerv1"
@ -341,6 +342,28 @@ func (ibm *providerIBM) NewClient(ctx context.Context, store esv1alpha1.GenericS
},
})
// Setup retry options, but only if present
if storeSpec.RetrySettings != nil {
var retryAmount int
var retryDuration time.Duration
if storeSpec.RetrySettings.MaxRetries != nil {
retryAmount = int(*storeSpec.RetrySettings.MaxRetries)
} else {
retryAmount = 3
}
if storeSpec.RetrySettings.RetryInterval != nil {
retryDuration, err = time.ParseDuration(*storeSpec.RetrySettings.RetryInterval)
} else {
retryDuration = 5 * time.Second
}
if err == nil {
secretsManager.Service.EnableRetries(retryAmount, retryDuration)
}
}
if err != nil {
return nil, fmt.Errorf(errIBMClient, err)
}

View file

@ -22,9 +22,13 @@ import (
"github.com/IBM/go-sdk-core/v5/core"
sm "github.com/IBM/secrets-manager-go-sdk/secretsmanagerv1"
"github.com/crossplane/crossplane-runtime/pkg/test"
corev1 "k8s.io/api/core/v1"
utilpointer "k8s.io/utils/pointer"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
fakesm "github.com/external-secrets/external-secrets/pkg/provider/ibm/fake"
)
@ -372,6 +376,54 @@ func TestGetSecretMap(t *testing.T) {
}
}
func TestValidRetryInput(t *testing.T) {
sm := providerIBM{}
invalid := "Invalid"
serviceURL := "http://fake-service-url.cool"
spec := &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
IBM: &esv1alpha1.IBMProvider{
Auth: esv1alpha1.IBMAuth{
SecretRef: esv1alpha1.IBMAuthSecretRef{
SecretAPIKey: v1.SecretKeySelector{
Name: "fake-secret",
Key: "fake-key",
},
},
},
ServiceURL: &serviceURL,
},
},
RetrySettings: &esv1alpha1.SecretStoreRetrySettings{
RetryInterval: &invalid,
},
},
}
expected := fmt.Sprintf("cannot setup new ibm client: time: invalid duration %q", invalid)
ctx := context.TODO()
kube := &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj kclient.Object) error {
if o, ok := obj.(*corev1.Secret); ok {
o.Data = map[string][]byte{
"fake-key": []byte("ImAFakeApiKey"),
}
return nil
}
return nil
}),
}
_, err := sm.NewClient(ctx, spec, kube, "default")
if !ErrorContains(err, expected) {
t.Errorf("CheckValidRetryInput unexpected error: %s, expected: '%s'", err.Error(), expected)
}
}
func ErrorContains(out error, want string) bool {
if out == nil {
return want == ""

View file

@ -16,20 +16,20 @@ package fake
import (
"context"
vault "github.com/oracle/oci-go-sdk/v45/vault"
secrets "github.com/oracle/oci-go-sdk/v45/secrets"
)
type OracleMockClient struct {
getSecret func(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error)
getSecret func(ctx context.Context, request secrets.GetSecretBundleByNameRequest) (response secrets.GetSecretBundleByNameResponse, err error)
}
func (mc *OracleMockClient) GetSecret(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error) {
func (mc *OracleMockClient) GetSecretBundleByName(ctx context.Context, request secrets.GetSecretBundleByNameRequest) (response secrets.GetSecretBundleByNameResponse, err error) {
return mc.getSecret(ctx, request)
}
func (mc *OracleMockClient) WithValue(input vault.GetSecretRequest, output vault.GetSecretResponse, err error) {
func (mc *OracleMockClient) WithValue(input secrets.GetSecretBundleByNameRequest, output secrets.GetSecretBundleByNameResponse, err error) {
if mc != nil {
mc.getSecret = func(ctx context.Context, paramReq vault.GetSecretRequest) (vault.GetSecretResponse, error) {
mc.getSecret = func(ctx context.Context, paramReq secrets.GetSecretBundleByNameRequest) (secrets.GetSecretBundleByNameResponse, error) {
return output, err
}
}

View file

@ -13,11 +13,12 @@ package oracle
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/oracle/oci-go-sdk/v45/common"
vault "github.com/oracle/oci-go-sdk/v45/vault"
"github.com/oracle/oci-go-sdk/v45/secrets"
"github.com/tidwall/gjson"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
@ -45,9 +46,10 @@ const (
errMissingTenancy = "missing Tenancy ID"
errMissingRegion = "missing Region"
errMissingFingerprint = "missing Fingerprint"
errMissingVault = "missing Vault"
errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
errMissingKey = "missing Key in secret: %s"
errInvalidSecret = "invalid secret received. no secret string nor binary for key: %s"
errUnexpectedContent = "unexpected secret bundle content"
)
type client struct {
@ -64,10 +66,11 @@ type client struct {
type VaultManagementService struct {
Client VMInterface
vault string
}
type VMInterface interface {
GetSecret(ctx context.Context, request vault.GetSecretRequest) (response vault.GetSecretResponse, err error)
GetSecretBundleByName(ctx context.Context, request secrets.GetSecretBundleByNameRequest) (secrets.GetSecretBundleByNameResponse, error)
}
func (c *client) setAuth(ctx context.Context) error {
@ -126,27 +129,32 @@ func (vms *VaultManagementService) GetSecret(ctx context.Context, ref esv1alpha1
if utils.IsNil(vms.Client) {
return nil, fmt.Errorf(errUninitalizedOracleProvider)
}
vmsRequest := vault.GetSecretRequest{
SecretId: &ref.Key,
}
secretOut, err := vms.Client.GetSecret(context.Background(), vmsRequest)
sec, err := vms.Client.GetSecretBundleByName(ctx, secrets.GetSecretBundleByNameRequest{
VaultId: &vms.vault,
SecretName: &ref.Key,
Stage: secrets.GetSecretBundleByNameStageEnum(ref.Version),
})
if err != nil {
return nil, util.SanitizeErr(err)
}
bt, ok := sec.SecretBundleContent.(secrets.Base64SecretBundleContentDetails)
if !ok {
return nil, fmt.Errorf(errUnexpectedContent)
}
payload, err := base64.StdEncoding.DecodeString(*bt.Content)
if err != nil {
return nil, err
}
if ref.Property == "" {
if *secretOut.SecretName != "" {
return []byte(*secretOut.SecretName), nil
}
return nil, fmt.Errorf(errInvalidSecret, ref.Key)
}
var payload *string
if secretOut.SecretName != nil {
payload = secretOut.SecretName
return payload, nil
}
payloadval := *payload
val := gjson.Get(string(payload), ref.Property)
val := gjson.Get(payloadval, ref.Property)
if !val.Exists() {
return nil, fmt.Errorf(errMissingKey, ref.Key)
}
@ -183,6 +191,10 @@ func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1alph
storeSpec := store.GetSpec()
oracleSpec := storeSpec.Provider.Oracle
if oracleSpec.Vault == "" {
return nil, fmt.Errorf(errMissingVault)
}
oracleStore := &client{
kube: kube,
store: oracleSpec,
@ -201,12 +213,15 @@ func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1alph
configurationProvider := common.NewRawConfigurationProvider(oracleTenancy, oracleUser, oracleRegion, oracleFingerprint, oraclePrivateKey, nil)
vaultManagementService, err := vault.NewVaultsClientWithConfigurationProvider(configurationProvider)
secretManagementService, err := secrets.NewSecretsClientWithConfigurationProvider(configurationProvider)
if err != nil {
return nil, fmt.Errorf(errOracleClient, err)
}
vms.Client = vaultManagementService
return vms, nil
return &VaultManagementService{
Client: secretManagementService,
vault: oracleSpec.Vault,
}, nil
}
func (vms *VaultManagementService) Close(ctx context.Context) error {

View file

@ -13,12 +13,13 @@ package oracle
import (
"context"
"encoding/base64"
"fmt"
"reflect"
"strings"
"testing"
vault "github.com/oracle/oci-go-sdk/v45/vault"
secrets "github.com/oracle/oci-go-sdk/v45/secrets"
utilpointer "k8s.io/utils/pointer"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@ -27,8 +28,8 @@ import (
type vaultTestCase struct {
mockClient *fakeoracle.OracleMockClient
apiInput *vault.GetSecretRequest
apiOutput *vault.GetSecretResponse
apiInput *secrets.GetSecretBundleByNameRequest
apiOutput *secrets.GetSecretBundleByNameResponse
ref *esv1alpha1.ExternalSecretDataRemoteRef
apiErr error
expectError string
@ -59,16 +60,16 @@ func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
}
}
func makeValidAPIInput() *vault.GetSecretRequest {
return &vault.GetSecretRequest{
SecretId: utilpointer.StringPtr("test-secret"),
func makeValidAPIInput() *secrets.GetSecretBundleByNameRequest {
return &secrets.GetSecretBundleByNameRequest{
SecretName: utilpointer.StringPtr("test-secret"),
VaultId: utilpointer.StringPtr("test-vault"),
}
}
func makeValidAPIOutput() *vault.GetSecretResponse {
return &vault.GetSecretResponse{
Etag: utilpointer.StringPtr("test-name"),
Secret: vault.Secret{},
func makeValidAPIOutput() *secrets.GetSecretBundleByNameResponse {
return &secrets.GetSecretBundleByNameResponse{
SecretBundle: secrets.SecretBundle{},
}
}
@ -98,12 +99,13 @@ func TestOracleVaultGetSecret(t *testing.T) {
// good case: default version is set
// key is passed in, output is sent back
setSecretString := func(smtc *vaultTestCase) {
smtc.apiOutput = &vault.GetSecretResponse{
Etag: utilpointer.StringPtr("test-name"),
Secret: vault.Secret{
CompartmentId: utilpointer.StringPtr("test-compartment-id"),
Id: utilpointer.StringPtr("test-id"),
SecretName: utilpointer.StringPtr("changedvalue"),
smtc.apiOutput = &secrets.GetSecretBundleByNameResponse{
SecretBundle: secrets.SecretBundle{
SecretId: utilpointer.StringPtr("test-id"),
VersionNumber: utilpointer.Int64(1),
SecretBundleContent: secrets.Base64SecretBundleContentDetails{
Content: utilpointer.StringPtr(base64.StdEncoding.EncodeToString([]byte(secretValue))),
},
},
}
smtc.expectedSecret = secretValue
@ -132,13 +134,17 @@ func TestOracleVaultGetSecret(t *testing.T) {
func TestGetSecretMap(t *testing.T) {
// good case: default version & deserialization
setDeserialization := func(smtc *vaultTestCase) {
smtc.apiOutput.SecretName = utilpointer.StringPtr(`{"foo":"bar"}`)
smtc.apiOutput.SecretBundleContent = secrets.Base64SecretBundleContentDetails{
Content: utilpointer.StringPtr(base64.StdEncoding.EncodeToString([]byte(`{"foo":"bar"}`))),
}
smtc.expectedData["foo"] = []byte("bar")
}
// bad case: invalid json
setInvalidJSON := func(smtc *vaultTestCase) {
smtc.apiOutput.SecretName = utilpointer.StringPtr(`-----------------`)
smtc.apiOutput.SecretBundleContent = secrets.Base64SecretBundleContentDetails{
Content: utilpointer.StringPtr(base64.StdEncoding.EncodeToString([]byte(`-----------------`))),
}
smtc.expectError = "unable to unmarshal secret"
}

View file

@ -26,5 +26,6 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
)

View file

@ -73,6 +73,7 @@ const (
errVaultRevokeToken = "error while revoking token: %w"
errUnknownCAProvider = "unknown caProvider type given"
errCANamespace = "cannot read secret for CAProvider due to missing namespace on kind ClusterSecretStore"
)
type Client interface {
@ -182,19 +183,40 @@ func (v *client) Close(ctx context.Context) error {
return nil
}
func (v *client) readSecret(ctx context.Context, path, version string) (map[string][]byte, error) {
kvPath := v.store.Path
func (v *client) buildPath(path string) string {
optionalMount := v.store.Path
origPath := strings.Split(path, "/")
newPath := make([]string, 0)
cursor := 0
if optionalMount != nil && origPath[0] != *optionalMount {
// Default case before path was optional
// Ensure that the requested path includes the SecretStores paths as prefix
newPath = append(newPath, *optionalMount)
} else {
newPath = append(newPath, origPath[cursor])
cursor++
}
if v.store.Version == esv1alpha1.VaultKVStoreV2 {
if !strings.HasSuffix(kvPath, "/data") {
kvPath = fmt.Sprintf("%s/data", kvPath)
// Add the required `data` part of the URL for the v2 API
if len(origPath) < 2 || origPath[1] != "data" {
newPath = append(newPath, "data")
}
}
newPath = append(newPath, origPath[cursor:]...)
returnPath := strings.Join(newPath, "/")
return returnPath
}
func (v *client) readSecret(ctx context.Context, path, version string) (map[string][]byte, error) {
dataPath := v.buildPath(path)
// path formated according to vault docs for v1 and v2 API
// v1: https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret
// v2: https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
req := v.client.NewRequest(http.MethodGet, fmt.Sprintf("/v1/%s/%s", kvPath, path))
req := v.client.NewRequest(http.MethodGet, fmt.Sprintf("/v1/%s", dataPath))
if version != "" {
req.Params.Set("version", version)
}
@ -258,6 +280,10 @@ func (v *client) newConfig() (*vault.Config, error) {
}
}
if v.store.CAProvider != nil && v.storeKind == esv1alpha1.ClusterSecretStoreKind && v.store.CAProvider.Namespace == nil {
return nil, errors.New(errCANamespace)
}
if v.store.CAProvider != nil {
var cert []byte
var err error
@ -290,10 +316,14 @@ func (v *client) newConfig() (*vault.Config, error) {
func getCertFromSecret(v *client) ([]byte, error) {
secretRef := esmeta.SecretKeySelector{
Name: v.store.CAProvider.Name,
Namespace: &v.store.CAProvider.Namespace,
Key: v.store.CAProvider.Key,
Name: v.store.CAProvider.Name,
Key: v.store.CAProvider.Key,
}
if v.store.CAProvider.Namespace != nil {
secretRef.Namespace = v.store.CAProvider.Namespace
}
ctx := context.Background()
res, err := v.secretKeyRef(ctx, &secretRef)
if err != nil {
@ -305,8 +335,11 @@ func getCertFromSecret(v *client) ([]byte, error) {
func getCertFromConfigMap(v *client) ([]byte, error) {
objKey := types.NamespacedName{
Namespace: v.store.CAProvider.Namespace,
Name: v.store.CAProvider.Name,
Name: v.store.CAProvider.Name,
}
if v.store.CAProvider.Namespace != nil {
objKey.Namespace = *v.store.CAProvider.Namespace
}
configMapRef := &corev1.ConfigMap{}
@ -628,7 +661,7 @@ func (v *client) requestTokenWithLdapAuth(ctx context.Context, client Client, ld
parameters := map[string]string{
"password": password,
}
url := strings.Join([]string{"/v1", "auth", "ldap", "login", username}, "/")
url := strings.Join([]string{"/v1", "auth", ldapAuth.Path, "login", username}, "/")
request := client.NewRequest("POST", url)
err = request.SetJSONBody(parameters)
@ -668,7 +701,7 @@ func (v *client) requestTokenWithJwtAuth(ctx context.Context, client Client, jwt
"role": role,
"jwt": jwt,
}
url := strings.Join([]string{"/v1", "auth", "jwt", "login"}, "/")
url := strings.Join([]string{"/v1", "auth", jwtAuth.Path, "login"}, "/")
request := client.NewRequest("POST", url)
err = request.SetJSONBody(parameters)

View file

@ -41,6 +41,10 @@ const (
secretDataString = "some-creds"
)
var (
secretStorePath = "secret"
)
func makeValidSecretStoreWithVersion(v esv1alpha1.VaultKVStoreVersion) *esv1alpha1.SecretStore {
return &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
@ -51,7 +55,7 @@ func makeValidSecretStoreWithVersion(v esv1alpha1.VaultKVStoreVersion) *esv1alph
Provider: &esv1alpha1.SecretStoreProvider{
Vault: &esv1alpha1.VaultProvider{
Server: "vault.example.com",
Path: "secret",
Path: &secretStorePath,
Version: v,
Auth: esv1alpha1.VaultAuth{
Kubernetes: &esv1alpha1.VaultKubernetesAuth{
@ -82,7 +86,7 @@ func makeValidSecretStoreWithCerts() *esv1alpha1.SecretStore {
Provider: &esv1alpha1.SecretStoreProvider{
Vault: &esv1alpha1.VaultProvider{
Server: "vault.example.com",
Path: "secret",
Path: &secretStorePath,
Version: esv1alpha1.VaultKVStoreV2,
Auth: esv1alpha1.VaultAuth{
Cert: &esv1alpha1.VaultCertAuth{
@ -119,6 +123,41 @@ func makeValidSecretStoreWithK8sCerts(isSecret bool) *esv1alpha1.SecretStore {
return store
}
func makeInvalidClusterSecretStoreWithK8sCerts() *esv1alpha1.ClusterSecretStore {
return &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
Kind: "ClusterSecretStore",
},
ObjectMeta: metav1.ObjectMeta{
Name: "vault-store",
Namespace: "default",
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
Vault: &esv1alpha1.VaultProvider{
Server: "vault.example.com",
Path: &secretStorePath,
Version: "v2",
Auth: esv1alpha1.VaultAuth{
Kubernetes: &esv1alpha1.VaultKubernetesAuth{
Path: "kubernetes",
Role: "kubernetes-auth-role",
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Name: "example-sa",
},
},
},
CAProvider: &esv1alpha1.CAProvider{
Name: "vault-cert",
Key: "cert",
Type: "Secret",
},
},
},
},
}
}
type secretStoreTweakFn func(s *esv1alpha1.SecretStore)
func makeSecretStore(tweaks ...secretStoreTweakFn) *esv1alpha1.SecretStore {
@ -352,6 +391,18 @@ MIICsTCCAZkCFEJJ4daz5sxkFlzq9n1djLEuG7bmMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMMCHZh
err: nil,
},
},
"GetCertNamespaceMissingError": {
reason: "Should return an error if namespace is missing and is a ClusterSecretStore",
args: args{
store: makeInvalidClusterSecretStoreWithK8sCerts(),
kube: &test.MockClient{
MockGet: test.NewMockGetFn(nil, kubeMockWithSecretTokenAndServiceAcc),
},
},
want: want{
err: errors.New(errCANamespace),
},
},
"GetCertSecretKeyMissingError": {
reason: "Should return an error if the secret key is missing",
args: args{
@ -627,3 +678,92 @@ func TestGetSecretMap(t *testing.T) {
})
}
}
func TestGetSecretPath(t *testing.T) {
storeV2 := makeValidSecretStore()
storeV2NoPath := storeV2.DeepCopy()
storeV2NoPath.Spec.Provider.Vault.Path = nil
storeV1 := makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV1)
storeV1NoPath := storeV1.DeepCopy()
storeV1NoPath.Spec.Provider.Vault.Path = nil
type args struct {
store *esv1alpha1.VaultProvider
path string
expected string
}
cases := map[string]struct {
reason string
args args
}{
"PathWithoutFormatV2": {
reason: "Data needs to be found in path",
args: args{
store: storeV2.Spec.Provider.Vault,
path: "secret/test",
expected: "secret/data/test",
},
},
"PathWithDataV2": {
reason: "Data needs to be found only once in path",
args: args{
store: storeV2.Spec.Provider.Vault,
path: "secret/data/test",
expected: "secret/data/test",
},
},
"PathWithoutFormatV2_NoPath": {
reason: "Data needs to be found in path and correct mountpoint is set",
args: args{
store: storeV2NoPath.Spec.Provider.Vault,
path: "secret/test",
expected: "secret/data/test",
},
},
"PathWithoutFormatV1": {
reason: "Data needs to be found in path",
args: args{
store: storeV1.Spec.Provider.Vault,
path: "secret/test",
expected: "secret/test",
},
},
"PathWithoutFormatV1_NoPath": {
reason: "Data needs to be found in path and correct mountpoint is set",
args: args{
store: storeV1NoPath.Spec.Provider.Vault,
path: "secret/test",
expected: "secret/test",
},
},
"WithoutPathButMountpointV2": {
reason: "Mountpoint needs to be set in addition to data",
args: args{
store: storeV2.Spec.Provider.Vault,
path: "test",
expected: "secret/data/test",
},
},
"WithoutPathButMountpointV1": {
reason: "Mountpoint needs to be set in addition to data",
args: args{
store: storeV1.Spec.Provider.Vault,
path: "test",
expected: "secret/test",
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
vStore := &client{
store: tc.args.store,
}
want := vStore.buildPath(tc.args.path)
if diff := cmp.Diff(want, tc.args.expected); diff != "" {
t.Errorf("\n%s\nvault.buildPath(...): -want expected, +got error:\n%s", tc.reason, diff)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show more