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:
commit
18a9bb1745
120 changed files with 6468 additions and 939 deletions
1
.github/PAUL.yaml
vendored
1
.github/PAUL.yaml
vendored
|
@ -13,6 +13,7 @@ maintainers:
|
|||
- Flydiverny
|
||||
- gabibeyer
|
||||
- ricardoptcosta
|
||||
- rodrmartinez
|
||||
# Allows for the /label and /remove-label commands
|
||||
# usage: /label enhancement
|
||||
# usage: /remove-label enhancement
|
||||
|
|
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
@ -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') }}
|
||||
|
|
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
@ -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
181
.github/workflows/e2e-managed.yml
vendored
Normal 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;
|
15
.github/workflows/e2e.yml
vendored
15
.github/workflows/e2e.yml
vendored
|
@ -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') }}
|
||||
|
|
2
.github/workflows/helm.yml
vendored
2
.github/workflows/helm.yml
vendored
|
@ -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
|
||||
|
|
35
.github/workflows/ok-to-test-managed.yml
vendored
Normal file
35
.github/workflows/ok-to-test-managed.yml
vendored
Normal 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
41
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -63,7 +63,6 @@ linters:
|
|||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
|
|
10
ADOPTERS.md
Normal file
10
ADOPTERS.md
Normal 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! :)
|
|
@ -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
|
||||
|
|
35
Makefile
35
Makefile
|
@ -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
|
||||
|
||||
|
|
17
README.md
17
README.md
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
101
apis/externalsecrets/v1alpha1/secretstore_webhook_types.go
Normal file
101
apis/externalsecrets/v1alpha1/secretstore_webhook_types.go
Normal 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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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: {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
|
133
docs/guides-gitops-using-fluxcd.md
Normal file
133
docs/guides-gitops-using-fluxcd.md
Normal 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)
|
26
docs/guides-using-latest-image.md
Normal file
26
docs/guides-using-latest-image.md
Normal 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
|
||||
```
|
|
@ -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).
|
||||
|
|
BIN
docs/pictures/screenshot_gitops_final_directory_tree.png
Normal file
BIN
docs/pictures/screenshot_gitops_final_directory_tree.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
120
docs/provider-webhook.md
Normal 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>
|
||||
```
|
||||
|
13
docs/snippets/azkv-secret-store-mi.yaml
Normal file
13
docs/snippets/azkv-secret-store-mi.yaml
Normal 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"
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
17
docs/snippets/gitops/crs/clusterSecretStore.yaml
Normal file
17
docs/snippets/gitops/crs/clusterSecretStore.yaml
Normal 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
|
5
docs/snippets/gitops/crs/kustomization.yaml
Normal file
5
docs/snippets/gitops/crs/kustomization.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- clusterSecretStore.yaml
|
13
docs/snippets/gitops/deployment-crds.yaml
Normal file
13
docs/snippets/gitops/deployment-crds.yaml
Normal 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
|
15
docs/snippets/gitops/deployment-crs.yaml
Normal file
15
docs/snippets/gitops/deployment-crs.yaml
Normal 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
|
28
docs/snippets/gitops/deployment.yaml
Normal file
28
docs/snippets/gitops/deployment.yaml
Normal 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
|
20
docs/snippets/gitops/kustomization.yaml
Normal file
20
docs/snippets/gitops/kustomization.yaml
Normal 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
|
4
docs/snippets/gitops/namespace.yaml
Normal file
4
docs/snippets/gitops/namespace.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: external-secrets
|
20
docs/snippets/gitops/repositories.yaml
Normal file
20
docs/snippets/gitops/repositories.yaml
Normal 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
|
8
docs/snippets/gitops/secret-token.yaml
Normal file
8
docs/snippets/gitops/secret-token.yaml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% raw %}
|
||||
# define your tempalte in a config map
|
||||
# define your template in a config map
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
1119
docs/spec.md
1119
docs/spec.md
File diff suppressed because it is too large
Load diff
BIN
e2e/.DS_Store
vendored
BIN
e2e/.DS_Store
vendored
Binary file not shown.
|
@ -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 \
|
||||
|
|
30
e2e/Makefile
30
e2e/Makefile
|
@ -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 \
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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" \
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
86
e2e/suite/gcpmanaged/gcpmanaged.go
Normal file
86
e2e/suite/gcpmanaged/gcpmanaged.go
Normal 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
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
247
go.mod
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
7
main.go
7
main.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 == ""
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue