diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f04682 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..53d3d10 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,103 @@ +name: Build artifacts + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +concurrency: build-${{ github.ref }} + +env: + REGISTRY: ghcr.io + +defaults: + run: + shell: bash + +jobs: + test: + name: Run tests + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + - name: Check that license header boilerplate is correct + uses: sap/cs-actions/check-go-license-boilerplate@main + with: + boilerplate-path: hack/boilerplate.go.txt + + - name: Check that license headers are correct + uses: sap/cs-actions/check-go-license-headers@main + with: + boilerplate-path: hack/boilerplate.go.txt + + - name: Run tests + run: | + make envtest + KUBEBUILDER_ASSETS=$(pwd)/bin/k8s/current E2E_ENABLED=${{ github.event_name == 'push' }} go test -count 1 ./... + + build-docker: + name: Build Docker image + runs-on: ubuntu-22.04 + needs: test + permissions: + contents: read + outputs: + image-archive: image.tar + image-repository: ${{ steps.prepare-repository-name.outputs.repository }} + image-tag: ${{ steps.extract-metadata.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Prepare repository name + id: prepare-repository-name + run: | + repository=$REGISTRY/${{ github.repository }} + echo "repository=${repository,,}" >> $GITHUB_OUTPUT + + - name: Extract metadata (tags, labels) for Docker + id: extract-metadata + uses: docker/metadata-action@v4 + with: + images: ${{ steps.prepare-repository-name.outputs.repository }} + + - name: Build Docker image + uses: docker/build-push-action@v4 + with: + platforms: linux/amd64,linux/arm64 + context: . + cache-from: | + type=gha,scope=sha-${{ github.sha }} + type=gha,scope=${{ github.ref_name }} + type=gha,scope=${{ github.base_ref || 'main' }} + type=gha,scope=main + cache-to: | + type=gha,scope=sha-${{ github.sha }},mode=max + type=gha,scope=${{ github.ref_name }},mode=max + outputs: | + type=oci,dest=${{ runner.temp }}/image.tar + tags: ${{ steps.extract-metadata.outputs.tags }} + labels: ${{ steps.extract-metadata.outputs.labels }} + + - name: Upload Docker image archive + uses: actions/upload-artifact@v3 + with: + name: image.tar + path: ${{ runner.temp }}/image.tar + diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..9ed0abe --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,170 @@ +name: Publish artifacts + +on: + release: + types: [published] + +concurrency: release-${{ github.event.release.tag_name }} + +env: + SEMVER_VERSION: 3.4.0 + REGISTRY: ghcr.io + # CHART_REPOSITORY: + # CHART_DIRECTORY: + +defaults: + run: + shell: bash + +jobs: + publish-docker: + name: Publish Docker image + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Prepare repository name + id: prepare-repository-name + run: | + repository=$REGISTRY/${{ github.repository }} + echo "repository=${repository,,}" >> $GITHUB_OUTPUT + + - name: Extract metadata (tags, labels) for Docker + id: extract-metadata + uses: docker/metadata-action@v4 + with: + images: ${{ steps.prepare-repository-name.outputs.repository }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + platforms: linux/amd64,linux/arm64 + context: . + cache-from: | + type=gha,scope=sha-${{ github.sha }} + type=gha,scope=${{ github.ref_name }} + type=gha,scope=${{ github.base_ref || 'main' }} + type=gha,scope=main + cache-to: | + type=gha,scope=sha-${{ github.sha }},mode=max + type=gha,scope=${{ github.ref_name }},mode=max + push: true + tags: ${{ steps.extract-metadata.outputs.tags }} + labels: ${{ steps.extract-metadata.outputs.labels }} + + update-chart: + name: Update Helm chart + runs-on: ubuntu-22.04 + needs: [publish-docker] + + steps: + - name: Prepare + id: prepare + run: | + chart_repository=$CHART_REPOSITORY + if [ -z "$chart_repository" ]; then + chart_repository=${{ github.repository }}-helm + fi + echo "chart_repository=$chart_repository" >> $GITHUB_OUTPUT + + chart_directory=$CHART_DIRECTORY + if [ -z "$chart_directory" ]; then + chart_directory=chart + fi + echo "chart_directory=$chart_directory" >> $GITHUB_OUTPUT + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Checkout chart repository + uses: actions/checkout@v4 + with: + repository: ${{ steps.prepare.outputs.chart_repository }} + path: chart-repository + token: ${{ secrets.WORKFLOW_USER_GH_TOKEN }} + + - name: Setup semver + uses: sap/cs-actions/setup-semver@main + with: + version: ${{ env.SEMVER_VERSION }} + install-directory: ${{ runner.temp }}/bin + + - name: Update chart repository + id: update + run: | + cd chart-repository + chart_directory=${{ steps.prepare.outputs.chart_directory }} + + old_version=$(yq .appVersion $chart_directory/Chart.yaml) + if [ "${old_version:0:1}" != v ] || [ "$(semver validate $old_version)" != valid ]; then + >&2 echo "Found invalid current appVersion ($old_version) in $chart_directory/Chart.yaml)." + exit 1 + fi + + new_version=${{ github.event.release.tag_name }} + if [ "${new_version:0:1}" != v ] || [ "$(semver validate $new_version)" != valid ]; then + >&2 echo "Invalid target appVersion ($new_version)." + exit 1 + fi + + if [ $(semver compare $new_version $old_version) -lt 0 ]; then + echo "Target appVersion ($new_version) is lower than current appVersion ($old_version); skipping update ..." + exit 0 + fi + + version_bump=$(semver diff $new_version $old_version) + echo "Found appVersion bump: $version_bump." + if [ "$version_bump" != major ] && [ "$version_bump" != minor ]; then + version_bump=patch + fi + echo "Performing chart version bump: $version_bump ..." + + echo "Updating appVersion in $chart_directory/Chart.yaml (current: $old_version; target: $new_version) ..." + perl -pi -e "s#^appVersion:.*#appVersion: $new_version#g" $chart_directory/Chart.yaml + + if [ -z "$(git status --porcelain)" ]; then + echo "Nothing has changed; skipping commit/push ..." + exit 0 + fi + + git config user.name "${{ vars.WORKFLOW_USER_NAME }}" + git config user.email "${{ vars.WORKFLOW_USER_EMAIL }}" + git add -A + git commit -F- <> $GITHUB_OUTPUT + + # add some safety sleep to overcome github race conditions + sleep 10s + + - name: Release chart repository + if: steps.update.outputs.version_bump != '' + uses: benc-uk/workflow-dispatch@v1 + with: + repo: ${{ steps.prepare.outputs.chart_repository }} + workflow: release.yaml + ref: main + token: ${{ secrets.WORKFLOW_USER_GH_TOKEN }} + inputs: '{ "version-bump": "${{ steps.update.outputs.version_bump }}" }' + diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..25e8a5b --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,157 @@ +name: Trigger release + +on: + workflow_dispatch: + inputs: + version-bump: + description: 'Whether to bump major, minor or patch version' + required: false + default: patch + type: choice + options: + - major + - minor + - patch + desired-version: + description: 'Version to be released; if specified, version-bump will be ignored' + required: false + default: '' + + schedule: + - cron: '10 7 * * 1' + - cron: '10 8 * * 1' + +concurrency: trigger-release + +env: + TAG_PREFIX: v + INITIAL_TAG: v0.1.0 + SEMVER_VERSION: 3.4.0 + +defaults: + run: + shell: bash + +jobs: + release: + name: Trigger release + runs-on: ubuntu-22.04 + permissions: + contents: write + + steps: + - name: Validate ref + run: | + if [ "${{ github.ref }}" != refs/heads/main ]; then + >&2 echo "Invalid ref: ${{ github.ref }} (expected: refs/heads/main)" + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.WORKFLOW_USER_GH_TOKEN }} + fetch-depth: 0 + + - name: Setup semver + uses: sap/cs-actions/setup-semver@main + with: + version: ${{ env.SEMVER_VERSION }} + install-directory: ${{ runner.temp }}/bin + + - name: Determine current release + id: get_current_release + uses: sap/cs-actions/get-highest-tag@main + with: + prefix: ${{ env.TAG_PREFIX }} + + - name: Determine target release + id: get_target_release + run: | + create_release=true + + if ${{ github.event_name == 'schedule' }}; then + commits_count=$(git rev-list --count --no-merges ${{ steps.get_current_release.outputs.tag }}..HEAD --before=1.hour) + if [ $commits_count -eq 0 ]; then + create_release=false + echo "There are no commits since latest release, nothing to do." + fi + version_bump=patch + else + version_bump=${{ inputs.version-bump }} + fi + + echo "Create release: $create_release" + echo "create_release=$create_release" >> $GITHUB_OUTPUT + + if [ "$create_release" != true ]; then + exit 0 + fi + + desired_version=${{ inputs.desired-version }} + current_version=${{ steps.get_current_release.outputs.version }} + + if [ -z "$desired_version" ]; then + case $version_bump in + major|minor|patch) + # ok + ;; + *) + >&2 echo "Invalid input: version-bump ($version_bump)" + exit 1 + esac + if [ -z "$current_version" ]; then + version=${INITIAL_TAG/#$TAG_PREFIX/} + tag=$INITIAL_TAG + else + version=$(semver bump $version_bump $current_version) + tag=$TAG_PREFIX$version + fi + else + if [[ $desired_version =~ ^$TAG_PREFIX([0-9].*)$ ]]; then + version=${BASH_REMATCH[1]} + tag=$desired_version + else + >&2 echo "Invalid input: desired-version ($desired_version) should start with $TAG_PREFIX." + exit 1 + fi + if [ "$(semver validate $version)" != valid ]; then + >&2 echo "Invalid input: desired-version ($version) is not a valid semantic version." + exit 1 + fi + if [ "$(semver compare $version $current_version)" -le 0 ]; then + >&2 echo "Invalid input: desired-version ($version) should be higher than current version ($current_version)." + exit 1 + fi + fi + + echo "Target version: $version" + echo "Target tag: $tag" + echo "version=$version" >> $GITHUB_OUTPUT + echo "tag=$tag" >> $GITHUB_OUTPUT + + - name: Determine target commit + id: get_target_commit + if: steps.get_target_release.outputs.create_release == 'true' + run: | + sha=$(git rev-parse HEAD) + echo "Target commit: $sha" + echo "sha=$sha" >> $GITHUB_OUTPUT + + - name: Wait for check suites to complete + if: steps.get_target_release.outputs.create_release == 'true' + uses: sap-contributions/await-check-suites@master + with: + ref: ${{ steps.get_target_commit.outputs.sha }} + intervalSeconds: 10 + timeoutSeconds: 1800 + failStepIfUnsuccessful: true + appSlugFilter: github-actions + + - name: Create Release + if: steps.get_target_release.outputs.create_release == 'true' + env: + GH_TOKEN: ${{ secrets.WORKFLOW_USER_GH_TOKEN }} + run: | + gh release create ${{ steps.get_target_release.outputs.tag }} \ + --target "${{ steps.get_target_commit.outputs.sha }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4456dbe --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +__debug_bin + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +.DS_Store + +# vscode stuff +.vscode/*.log + +# temp stuff +/tmp + +# local stuff +/.kubeconfig +/.local/ssl diff --git a/.local/README.md b/.local/README.md new file mode 100644 index 0000000..88d5abe --- /dev/null +++ b/.local/README.md @@ -0,0 +1,16 @@ +# Instructions for local development + +Prerequisite: K8s cluster (kind, minikube) with cert-manager installed. + +1. Deploy webhook definitions and according objects: + ```bash + # replace HOST_IP below with a non-loopback interface address of your desktop + HOST_IP=1.2.3.4 envsubst < .local/k8s-resources.yaml | kubectl apply -f - + ``` + +2. Extract the TLS server keypair: + ```bash + .local/getcerts.sh + ``` + +Afterwards (if using vscode) it should be possible to start the operator with the included launch configuration. diff --git a/.local/getcerts.sh b/.local/getcerts.sh new file mode 100755 index 0000000..ed209b7 --- /dev/null +++ b/.local/getcerts.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -eo pipefail + +cd "$(dirname "$0")" + +mkdir -p ssl + +kubectl get secret secret-generator-webhook -o jsonpath='{.data.tls\.key}' | base64 -d > ssl/tls.key +kubectl get secret secret-generator-webhook -o jsonpath='{.data.tls\.crt}' | base64 -d > ssl/tls.crt diff --git a/.local/k8s-resources.yaml b/.local/k8s-resources.yaml new file mode 100644 index 0000000..4b8a68c --- /dev/null +++ b/.local/k8s-resources.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: secret-generator-webhook + namespace: default +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 2443 +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: secret-generator-webhook + namespace: default +subsets: +- addresses: + - ip: ${HOST_IP} + ports: + - port: 2443 +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: secret-generator-webhook + namespace: default +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: secret-generator-webhook + namespace: default +spec: + dnsNames: + - secret-generator-webhook + - secret-generator-webhook.default + - secret-generator-webhook.default.svc + - secret-generator-webhook.default.svc.cluster.local + issuerRef: + name: secret-generator-webhook + secretName: secret-generator-webhook +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: secret-generator-webhook + annotations: + cert-manager.io/inject-ca-from: default/secret-generator-webhook +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: secret-generator-webhook + namespace: default + path: /core/v1/secret/mutate + port: 443 + name: mutate.secrets.core.k8s.io + rules: + - apiGroups: + - '' + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - secrets + scope: Namespaced + objectSelector: + matchExpressions: + - key: secret-generator.cs.sap.com/enabled + operator: In + values: + - 'true' + namespaceSelector: + matchPolicy: Equivalent + sideEffects: None + timeoutSeconds: 10 + failurePolicy: Fail + reinvocationPolicy: Never diff --git a/.reuse/dep5 b/.reuse/dep5 index 96d499f..f394dc3 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: -Upstream-Contact: -Source: +Upstream-Name: secret-generator +Upstream-Contact: SAP_ERP4SME_DevOps@sap.com +Source: https://github.com/sap/secret-generator Disclaimer: The code in this project may include calls to APIs ("API Calls") of SAP or third-party products or services developed outside of this project ("External Products"). @@ -24,14 +24,6 @@ Disclaimer: The code in this project may include calls to APIs ("API Calls") of you any rights to use or access any SAP External Product, or provide any third parties the right to use of access any SAP External Product, through API Calls. -Files: -Copyright: SAP SE or an SAP affiliate company and contributors +Files: ** +Copyright: 2023 SAP SE or an SAP affiliate company and secret-generator contributors License: Apache-2.0 - -Files: -Copyright: -License: - -Files: -Copyright: -License: \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9d7afe1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Operator", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/webhook", + "args": [ + "--bind-address=:2443", + "--tls-key-file=${workspaceFolder}/.local/ssl/tls.key", + "--tls-cert-file=${workspaceFolder}/.local/ssl/tls.crt" + ] + } + ] +} + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6c33fe3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "go.testEnvVars": { + "KUBEBUILDER_ASSETS": "${workspaceFolder}/bin/k8s/current" + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..96feda7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +### build go executable +FROM --platform=$BUILDPLATFORM golang:1.21.3 as build +ARG TARGETOS TARGETARCH + +WORKDIR /workspace + +COPY go.mod go.mod +COPY go.sum go.sum +RUN go mod download + +COPY cmd/ cmd/ +COPY internal/ internal/ +COPY Makefile Makefile + +RUN make envtest \ + && CGO_ENABLED=0 KUBEBUILDER_ASSETS="/workspace/bin/k8s/current" go test ./... \ + && CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o ./bin/webhook ./cmd/webhook + +### final image +FROM scratch + +ENTRYPOINT ["/app/bin/webhook"] + +COPY --from=build /workspace/bin/webhook /app/bin/webhook diff --git a/LICENSE b/LICENSE index 261eeb9..d9a10c0 100644 --- a/LICENSE +++ b/LICENSE @@ -174,28 +174,3 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b7ea05d --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(LOCALBIN)/k8s/current" go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: fmt vet ## Build webhook binary. + go build -o bin/webhook ./cmd/webhook + +.PHONY: run +run: fmt vet ## Run a controller from your host. + go run ./cmd/webhook + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Versions +ENVTEST_K8S_VERSION = 1.25.0 + +## Tool Binaries +ENVTEST ?= $(LOCALBIN)/setup-envtest + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + ENVTESTDIR=$$($(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path) ;\ + chmod -R u+w $$ENVTESTDIR ;\ + rm -f $(LOCALBIN)/k8s/current ;\ + ln -s $$ENVTESTDIR $(LOCALBIN)/k8s/current diff --git a/README.md b/README.md index 9721fd8..92acb2b 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,68 @@ -# SAP Repository Template +# Kubernetes Secret Generator -Default templates for SAP open source repositories, including LICENSE, .reuse/dep5, Code of Conduct, etc... All repositories on github.com/SAP will be created based on this template. +[![REUSE status](https://api.reuse.software/badge/github.com/SAP/secret-generator)](https://api.reuse.software/info/github.com/SAP/secret-generator) -## To-Do +## About this project -In case you are the maintainer of a new SAP open source project, these are the steps to do with the template files: +This repository contains a [Mutating Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers) for Kubernetes secrets that allows to generate certain secret values (e.g. passwords) upon first appearance of the according secret key. For example: -- Check if the default license (Apache 2.0) also applies to your project. A license change should only be required in exceptional cases. If this is the case, please change the [license file](LICENSE). -- Enter the correct metadata for the REUSE tool. See our [wiki page](https://wiki.wdf.sap.corp/wiki/display/ospodocs/Using+the+Reuse+Tool+of+FSFE+for+Copyright+and+License+Information) for details how to do it. You can find an initial .reuse/dep5 file to build on. Please replace the parts inside the single angle quotation marks < > by the specific information for your repository and be sure to run the REUSE tool to validate that the metadata is correct. -- Adjust the contribution guidelines (e.g. add coding style guidelines, pull request checklists, different license if needed etc.) -- Add information about your project to this README (name, description, requirements etc). Especially take care for the placeholders - those ones need to be replaced with your project name. See the sections below the horizontal line and [our guidelines on our wiki page](https://wiki.wdf.sap.corp/wiki/display/ospodocs/Guidelines+for+README.md+file) what is required and recommended. -- Remove all content in this README above and including the horizontal line ;) +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-secret + labels: + secret-generator.cs.sap.com/enabled: "true" +stringData: + my-password: "%generate:password:length=16" + my-uuid: "%generate:uuid" + my-other-key: "some static value" +``` -*** +To make it clear, the generation of a value only happens if the according key is not present in the secret. Existing values will never be touched (even if the `%generate` clause changes). -# Our new open source project +By default - when using the [Helm chart](https://github.com/sap/secret-generator-helm) - the webhook is called for secrets having the label `secret-generator.cs.sap.com/enabled: "true"`, but this can be overridden in the chart's configuration. -## About this project +Then, secret values of the form `%generate:[:;;...]` will be replaced accordingly. +Currently, two generator types are supported: `uuid` and `password`: +- `uuid` will generate a [RFC4122](https://datatracker.ietf.org/doc/html/rfc4122) UUIDv4 and allows the following arguments: + - `encoding=`: encoding to be applied to the generated uuid (note: use raw for no padding) +- `password` allows the following arguments: + - `length=<1-99>`: length of the generated password (default 32) + - `num_digits=<0-99>`: number of digits (0-9) in the generated password (default length/4) + - `num_symbols=<0-99>`: number of symbols in the generated pasasword (default length/4) + - `symbols=`: symbols (i.e. non-alphanumerics) to be used in the generated password (default: `~!@#$%^&*()_+-={}|:<>?,./`) + - `encoding=`: encoding to be applied to the generated password (note: the actual length will be larger than specified by length then). + +As a short form it is possible to just specify `%generate` as secret value, in which case a (32 character) password will be generated. + +**Command line flags** + +|Flag |Optional|Default|Description | +|-----------------------------|--------|-------|------------------------------------------------------------| +|-bind-address string |yes |:2443 |Webhook bind address | +|-tls-key-file |no |- |File containing the TLS private key used for SSL termination| +|-tls-cert-file |no |- |File containing the TLS certificate matching the private key| -*Insert a short description of your project here...* +**References** + +- Password generation uses [github.com/sethvargo/go-password/password](https://pkg.go.dev/github.com/sethvargo/go-password) + +- UUID generation uses [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ## Requirements and Setup -*Insert a short description what is required to get your project running...* +The recommended deployment method is to use the [Helm chart](https://github.com/sap/secret-generator-helm): + +```bash +helm upgrade -i secret-generator oci://ghcr.io/sap/secret-generator-helm/secret-generator +``` + +The API reference is here: [https://pkg.go.dev/github.com/sap/secret-generator](https://pkg.go.dev/github.com/sap/secret-generator). ## Support, Feedback, Contributing -This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/SAP//issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). +This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/SAP/secret-generator/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). ## Code of Conduct @@ -34,4 +70,4 @@ We as members, contributors, and leaders pledge to make participation in our com ## Licensing -Copyright (20xx-)20xx SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/). +Copyright 2023 SAP SE or an SAP affiliate company and secret-generator contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/secret-generator). diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go new file mode 100644 index 0000000..1ffe52b --- /dev/null +++ b/cmd/webhook/main.go @@ -0,0 +1,43 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "context" + "flag" + + "github.com/pkg/errors" + "github.com/spf13/pflag" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/klog/v2" + + "github.com/sap/admission-webhook-runtime/pkg/admission" + + "github.com/sap/secret-generator/internal/webhook" +) + +func main() { + pflag.CommandLine.AddGoFlagSet(admission.FlagSet()) + klog.InitFlags(nil) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.CommandLine.SortFlags = false + pflag.Parse() + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + klog.Fatal(errors.Wrap(err, "error populating corev1 scheme")) + } + webhook := webhook.NewSecretWebhook() + if err := admission.RegisterMutatingWebhook[*corev1.Secret](webhook, scheme, klog.NewKlogr()); err != nil { + klog.Fatal(errors.Wrapf(err, "error registering webhook for corev1.Secret")) + } + if err := admission.Serve(context.Background(), nil); err != nil { + klog.Fatal(errors.Wrap(err, "error running http server")) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a747f8a --- /dev/null +++ b/go.mod @@ -0,0 +1,65 @@ +module github.com/sap/secret-generator + +go 1.21 + +require ( + github.com/google/uuid v1.3.1 + github.com/onsi/ginkgo/v2 v2.12.1 + github.com/onsi/gomega v1.28.0 + github.com/pkg/errors v0.9.1 + github.com/sap/admission-webhook-runtime v0.1.5 + github.com/sethvargo/go-password v0.2.0 + github.com/spf13/pflag v1.0.5 + k8s.io/api v0.28.2 + k8s.io/apimachinery v0.28.2 + k8s.io/client-go v0.28.2 + k8s.io/klog/v2 v2.100.1 + sigs.k8s.io/controller-runtime v0.16.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/zapr v1.2.4 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.25.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.12.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.28.0 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5933b15 --- /dev/null +++ b/go.sum @@ -0,0 +1,235 @@ +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= +github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c= +github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sap/admission-webhook-runtime v0.1.5 h1:DTDzZQZ/FtNtRKS9g7kKuWgmm73TtoIRDwFddxTJtTs= +github.com/sap/admission-webhook-runtime v0.1.5/go.mod h1:4MtypwYvxSz18gc388XIt3UJQX3Z6QEoGDSoUqd7UtE= +github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= +github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= +k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= +k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= +k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= +k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= +k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= +k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= +k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= +k8s.io/component-base v0.28.1 h1:LA4AujMlK2mr0tZbQDZkjWbdhTV5bRyEyAFe0TJxlWg= +k8s.io/component-base v0.28.1/go.mod h1:jI11OyhbX21Qtbav7JkhehyBsIRfnO8oEgoAR12ArIU= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU= +sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..e9c2b52 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,4 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ diff --git a/internal/webhook/mutator.go b/internal/webhook/mutator.go new file mode 100644 index 0000000..b35705d --- /dev/null +++ b/internal/webhook/mutator.go @@ -0,0 +1,178 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package webhook + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/sethvargo/go-password/password" + + corev1 "k8s.io/api/core/v1" +) + +const ( + AnnotationKeyPrefix = "secret-generator.cs.sap.com/prefix" + DefaultPrefix = "%generate" + Symbols = `-~!@#$%^&*()_+={}|:<>?,./` // caveat: important to have - at first place (to work in regexp character sets) +) + +func handleCreateSecret(secret *corev1.Secret) error { + prefix := DefaultPrefix + if v, ok := secret.Annotations[AnnotationKeyPrefix]; ok { + prefix = v + } + for k := range secret.Data { + if format, ok := parseValue(string(secret.Data[k]), prefix); ok { + generatedValue, err := generateValue(format) + if err != nil { + return errors.Wrapf(err, "error generating value for key '%s'", k) + } + secret.Data[k] = []byte(generatedValue) + } + } + return nil +} + +func handleUpdateSecret(secret *corev1.Secret, oldSecret *corev1.Secret) error { + prefix := DefaultPrefix + if v, ok := secret.Annotations[AnnotationKeyPrefix]; ok { + prefix = v + } + for k := range secret.Data { + if format, ok := parseValue(string(secret.Data[k]), prefix); ok { + if v, ok := oldSecret.Data[k]; ok { + secret.Data[k] = v + } else { + generatedValue, err := generateValue(format) + if err != nil { + return errors.Wrapf(err, "error generating value for key '%s'", k) + } + secret.Data[k] = []byte(generatedValue) + } + } + } + return nil +} + +func parseValue(value string, prefix string) (string, bool) { + if value == prefix { + return "", true + } else if strings.HasPrefix(value, prefix+":") { + return strings.TrimPrefix(value, prefix+":"), true + } else { + return "", false + } +} + +func generateValue(format string) (string, error) { + if format == "" || format == ":" { + format = "password" + } + m := regexp.MustCompile(`^([^:]+)(?::(.*))?$`).FindStringSubmatch(format) + generatorType := m[1] + generatorArgs := m[2] + var generatedValue string + var generationError error + switch generatorType { + case "password": + length := 32 + symbols := Symbols + numDigits := -1 + numSymbols := -1 + encoding := "" + if generatorArgs != "" { + for _, arg := range strings.Split(generatorArgs, ";") { + if m := regexp.MustCompile(`^length=(\d+)$`).FindStringSubmatch(arg); m != nil { + length, _ = strconv.Atoi(m[1]) + } else if m := regexp.MustCompile(`^symbols=([` + Symbols + `]+)$`).FindStringSubmatch(arg); m != nil { + symbols = normalizeSymbols(m[1]) + } else if m := regexp.MustCompile(`^num_digits=(\d{1,2})$`).FindStringSubmatch(arg); m != nil { + numDigits, _ = strconv.Atoi(m[1]) + } else if m := regexp.MustCompile(`^num_symbols=(\d{1,2})$`).FindStringSubmatch(arg); m != nil { + numSymbols, _ = strconv.Atoi(m[1]) + } else if m := regexp.MustCompile(`^encoding=(.+)$`).FindStringSubmatch(arg); m != nil { + encoding = m[1] + } else { + return "", fmt.Errorf("invalid password generator argument: %s", arg) + } + } + } + if numDigits < 0 { + numDigits = length / 4 + } + if numSymbols < 0 { + numSymbols = length / 4 + } + value, err := generatePassword(length, numDigits, numSymbols, symbols) + if err != nil { + return "", err + } + if encoding == "" { + generatedValue = value + } else { + generatedValue, generationError = encode(encoding, []byte(value)) + } + case "uuid": + encoding := "" + if generatorArgs != "" { + for _, arg := range strings.Split(generatorArgs, ";") { + if m := regexp.MustCompile(`^encoding=(.+)$`).FindStringSubmatch(arg); m != nil { + encoding = m[1] + } else { + return "", fmt.Errorf("invalid uuid generator argument: %s", arg) + } + } + } + generatedUuid := uuid.New() + if encoding == "" { + generatedValue = generatedUuid.String() + } else { + generatedValue, generationError = encode(encoding, generatedUuid[:]) + } + + default: + return "", fmt.Errorf("unsupported generator type: %s", generatorType) + } + return generatedValue, generationError +} + +func encode(encoding string, value []byte) (string, error) { + var encodedValue string + var err error + + switch encoding { + case "base32": + encodedValue = base32.StdEncoding.EncodeToString(value) + case "base64": + encodedValue = base64.StdEncoding.EncodeToString(value) + case "base64_url": + encodedValue = base64.URLEncoding.EncodeToString(value) + case "base64_raw": + encodedValue = base64.RawStdEncoding.EncodeToString(value) + case "base64_raw_url": + encodedValue = base64.RawURLEncoding.EncodeToString(value) + default: + err = fmt.Errorf("unsupported encoding %s", encoding) + } + + return encodedValue, err +} + +func generatePassword(length int, numDigits int, numSymbols int, symbols string) (string, error) { + input := &password.GeneratorInput{Symbols: symbols} + generator, err := password.NewGenerator(input) + if err != nil { + return "", err + } + return generator.Generate(length, numDigits, numSymbols, false, true) +} diff --git a/internal/webhook/mutator_test.go b/internal/webhook/mutator_test.go new file mode 100644 index 0000000..ab8acac --- /dev/null +++ b/internal/webhook/mutator_test.go @@ -0,0 +1,365 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package webhook + +import ( + "encoding/base32" + "encoding/base64" + "regexp" + "testing" + + "github.com/google/uuid" + + corev1 "k8s.io/api/core/v1" +) + +func TestHandleCreateSecret(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + "key1": []byte("%generate"), + "key2": []byte("%generate:password:length=8"), + "key3": []byte("%generate:uuid"), + "key4": []byte("value"), + }, + } + if err := handleCreateSecret(secret); err != nil { + t.Fatalf("handleCreateSecret: got errror: %s", err) + } + if s := string(secret.Data["key1"]); len(s) != 32 { + t.Errorf("handleCreateSecret: got invalid password: %s", s) + } + if s := string(secret.Data["key2"]); len(s) != 8 { + t.Errorf("handleCreateSecret: got invalid password: %s", s) + } + if _, err := uuid.Parse(string(secret.Data["key3"])); err != nil { + t.Errorf("handleCreateSecret: got invalid uuid; error: %s", err) + } + if s := string(secret.Data["key4"]); s != "value" { + t.Errorf("handleCreateSecret: got invalid unmanaged value: %s", s) + } +} + +func TestHandleCreateSecretWithError(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + "key1": []byte("%generate:foobar"), + }, + } + if err := handleCreateSecret(secret); err == nil { + t.Error("handleCreateSecret: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } +} + +func TestHandleUpdateSecret(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + "key1": []byte("%generate"), + "key2": []byte("%generate:password:length=8"), + "key3": []byte("%generate:uuid"), + "key4": []byte("value"), + "existingKey1": []byte("%generate"), + "existingKey2": []byte("%generate:password:length=8"), + "existingKey3": []byte("%generate:uuid"), + "existingKey4": []byte("value"), + }, + } + oldSecret := &corev1.Secret{ + Data: map[string][]byte{ + "existingKey1": []byte("ABCDEFGHIJKLMabcdefghijklm012345"), + "existingKey2": []byte("ABCabc12"), + "existingKey3": []byte("eb89b65f-cd54-40b4-8122-e8d42ce3c324"), + "existingKey4": []byte("value"), + }, + } + if err := handleUpdateSecret(secret, oldSecret); err != nil { + t.Fatalf("handleUpdateSecret: got errror: %s", err) + } + if s := string(secret.Data["key1"]); len(s) != 32 { + t.Errorf("handleUpdateSecret: got invalid password: %s", s) + } + if s := string(secret.Data["key2"]); len(s) != 8 { + t.Errorf("handleUpdateSecret: got invalid password: %s", s) + } + if _, err := uuid.Parse(string(secret.Data["key3"])); err != nil { + t.Errorf("handleUpdateSecret: got invalid uuid; error: %s", err) + } + if s := string(secret.Data["key4"]); s != "value" { + t.Errorf("handleUpdateSecret: got invalid unmanaged value: %s", s) + } + if string(secret.Data["existingKey1"]) != string(oldSecret.Data["existingKey1"]) { + t.Error("handleUpdateSecret: existing value got changed") + } + if string(secret.Data["existingKey2"]) != string(oldSecret.Data["existingKey2"]) { + t.Error("handleUpdateSecret: existing value got changed") + } + if string(secret.Data["existingKey3"]) != string(oldSecret.Data["existingKey3"]) { + t.Error("handleUpdateSecret: existing value got changed") + } + if string(secret.Data["existingKey4"]) != string(oldSecret.Data["existingKey4"]) { + t.Error("handleUpdateSecret: existing value got changed") + } +} + +func TestHandleUpdateSecretWithError(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + "key1": []byte("%generate:foobar"), + }, + } + oldSecret := &corev1.Secret{} + if err := handleUpdateSecret(secret, oldSecret); err == nil { + t.Error("handleUpdateSecret: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } +} + +func TestGenerateValue(t *testing.T) { + var v string + var err error + + // short form; will be interpreted as password without arguments + v, err = generateValue("") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if !regexp.MustCompile(`^[A-Za-z0-9` + Symbols + `]{32}$`).MatchString(v) { + t.Errorf("generateValue: got invalid password (wrong length): %s", v) + } + if len(regexp.MustCompile(`[A-Za-z]`).FindAllString(v, -1)) != 16 { + t.Errorf("generateValue: got invalid password (wrong letter count): %s", v) + } + if len(regexp.MustCompile(`[0-9]`).FindAllString(v, -1)) != 8 { + t.Errorf("generateValue: got invalid password (wrong digit count): %s", v) + } + if len(regexp.MustCompile(`[`+Symbols+`]`).FindAllString(v, -1)) != 8 { + t.Errorf("generateValue: got invalid password (wrong symbol count): %s", v) + } + + // short form; will be interpreted as password without arguments + v, err = generateValue(":") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if !regexp.MustCompile(`^[A-Za-z0-9` + Symbols + `]{32}$`).MatchString(v) { + t.Errorf("generateValue: got invalid password (wrong length): %s", v) + } + if len(regexp.MustCompile(`[A-Za-z]`).FindAllString(v, -1)) != 16 { + t.Errorf("generateValue: got invalid password (wrong letter count): %s", v) + } + if len(regexp.MustCompile(`[0-9]`).FindAllString(v, -1)) != 8 { + t.Errorf("generateValue: got invalid password (wrong digit count): %s", v) + } + if len(regexp.MustCompile(`[`+Symbols+`]`).FindAllString(v, -1)) != 8 { + t.Errorf("generateValue: got invalid password (wrong symbol count): %s", v) + } + + // password without arguments + v, err = generateValue("password") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if !regexp.MustCompile(`^[A-Za-z0-9` + Symbols + `]{32}$`).MatchString(v) { + t.Errorf("generateValue: got invalid password (wrong length): %s", v) + } + if len(regexp.MustCompile(`[A-Za-z]`).FindAllString(v, -1)) != 16 { + t.Errorf("generateValue: got invalid password (wrong letter count): %s", v) + } + if len(regexp.MustCompile(`[0-9]`).FindAllString(v, -1)) != 8 { + t.Errorf("generateValue: got invalid password (wrong digit count): %s", v) + } + if len(regexp.MustCompile(`[`+Symbols+`]`).FindAllString(v, -1)) != 8 { + t.Errorf("generateValue: got invalid password (wrong symbol count): %s", v) + } + + // password with arguments + symbols := "_-" + v, err = generateValue("password:length=20;num_digits=3;num_symbols=4;symbols=" + symbols) + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if !regexp.MustCompile(`^[A-Za-z0-9` + symbols + `]{20}$`).MatchString(v) { + t.Errorf("generateValue: got invalid password (wrong length): %s", v) + } + if len(regexp.MustCompile(`[A-Za-z]`).FindAllString(v, -1)) != 13 { + t.Errorf("generateValue: got invalid password (wrong letter count): %s", v) + } + if len(regexp.MustCompile(`[0-9]`).FindAllString(v, -1)) != 3 { + t.Errorf("generateValue: got invalid password (wrong digit count): %s", v) + } + if len(regexp.MustCompile(`[`+symbols+`]`).FindAllString(v, -1)) != 4 { + t.Errorf("generateValue: got invalid password (wrong symbol count): %s", v) + } + + // password with base32 encoding + v, err = generateValue("password:length=5;num_digits=0;num_symbols=5;symbols=_;encoding=base32") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if v != "L5PV6X27" { + // L5PV6X27 is base32 of _____ + t.Errorf("generateValue: got invalid password (invalid base64 encoding): %s", v) + } + + // password with base64 encoding + v, err = generateValue("password:length=5;num_digits=0;num_symbols=5;symbols=_;encoding=base64") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if v != "X19fX18=" { + // X19fX18= is base64 of _____ + t.Errorf("generateValue: got invalid password (invalid base64 encoding): %s", v) + } + + // password with base64_raw (without padding) encoding + v, err = generateValue("password:length=5;num_digits=0;num_symbols=5;symbols=_;encoding=base64_raw") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if v != "X19fX18" { + // X19fX18 is base64_raw of _____ + t.Errorf("generateValue: got invalid password (invalid base64 encoding): %s", v) + } + + // uuid + v, err = generateValue("uuid") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + if _, err := uuid.Parse(string(v)); err != nil { + t.Errorf("generateValue: got invalid uuid; error: %s", err) + } + + // uuid encoding base32 + v, err = generateValue("uuid:encoding=base32") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + var decodedUuidBytes []byte + decodedUuidBytes, _ = base32.StdEncoding.DecodeString(v) + if _, err := uuid.FromBytes(decodedUuidBytes); err != nil { + t.Errorf("generateValue: got invalid uuid; error: %s", err) + } + + // uuid encoding base64 + v, err = generateValue("uuid:encoding=base64") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + decodedUuidBytes, _ = base64.StdEncoding.DecodeString(v) + if _, err := uuid.FromBytes(decodedUuidBytes); err != nil { + t.Errorf("generateValue: got invalid uuid; error: %s", err) + } + + // uuid encoding base64 url + v, err = generateValue("uuid:encoding=base64_url") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + decodedUuidBytes, _ = base64.URLEncoding.DecodeString(v) + if _, err := uuid.FromBytes(decodedUuidBytes); err != nil { + t.Errorf("generateValue: got invalid uuid; error: %s", err) + } + + // uuid encoding base64_raw (without padding) + v, err = generateValue("uuid:encoding=base64_raw") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + decodedUuidBytes, _ = base64.RawStdEncoding.DecodeString(v) + if _, err := uuid.FromBytes(decodedUuidBytes); err != nil { + t.Errorf("generateValue: got invalid uuid; error: %s", err) + } + + // uuid encoding base64_raw url (without padding) + v, err = generateValue("uuid:encoding=base64_raw_url") + if err != nil { + t.Fatalf("generateValue: got errror: %s", err) + } + decodedUuidBytes, _ = base64.RawURLEncoding.DecodeString(v) + if _, err := uuid.FromBytes(decodedUuidBytes); err != nil { + t.Errorf("generateValue: got invalid uuid; error: %s", err) + } + +} + +func TestGenerateValueWithError(t *testing.T) { + var err error + + // invalid generator + _, err = generateValue("foobar") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid password argument + _, err = generateValue("password:foo=bar") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid password argument: length + _, err = generateValue("password:length=foo") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid password argument: number of digits + _, err = generateValue("password:num_digits=foo") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid password argument: number of symbols + _, err = generateValue("password:num_symbols=foo") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid password argument: number of symbols + _, err = generateValue("password:symbols=foo") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid password argument: encoding + _, err = generateValue("password:encoding=foo") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // error during password generation: too many digits/symbols (symbols will default to 4/4 = 1 here) + _, err = generateValue("password:length=4;num_digits=4") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } + + // invalid uuid argument + _, err = generateValue("uuid:foo=bar") + if err == nil { + t.Error("generateValue: expected error, but got none") + } else { + t.Logf("ok; got error: %s", err) + } +} diff --git a/internal/webhook/suite_test.go b/internal/webhook/suite_test.go new file mode 100644 index 0000000..b26cd53 --- /dev/null +++ b/internal/webhook/suite_test.go @@ -0,0 +1,288 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package webhook_test + +import ( + "context" + "crypto/tls" + "encoding/base32" + "encoding/base64" + "fmt" + "maps" + "net" + "sync" + "testing" + "time" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/sap/admission-webhook-runtime/pkg/admission" + + "github.com/sap/secret-generator/internal/webhook" +) + +func TestWebhook(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Webhook Suite") +} + +var testEnv *envtest.Environment +var cfg *rest.Config +var ctx context.Context +var cancel context.CancelFunc +var threads sync.WaitGroup +var clientset kubernetes.Interface + +const testingNamespace = "testing" + +var _ = BeforeSuite(func() { + log.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + var err error + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + WebhookInstallOptions: envtest.WebhookInstallOptions{ + MutatingWebhooks: []*admissionv1.MutatingWebhookConfiguration{ + buildMutatingWebhookConfiguration(), + }, + }, + } + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + webhookInstallOptions := &testEnv.WebhookInstallOptions + + By("initializing kubernetes clientset") + clientset, err = kubernetes.NewForConfig(cfg) + Expect(err).NotTo(HaveOccurred()) + + By("initializing webhook scheme") + scheme := runtime.NewScheme() + err = corev1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + By("registering webhook") + err = admission.RegisterMutatingWebhook[*corev1.Secret](webhook.NewSecretWebhook(), scheme, log.Log) + Expect(err).NotTo(HaveOccurred()) + + By("starting webhook server") + threads.Add(1) + go func() { + defer threads.Done() + defer GinkgoRecover() + options := &admission.ServeOptions{ + BindAddress: fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort), + CertFile: webhookInstallOptions.LocalServingCertDir + "/tls.crt", + KeyFile: webhookInstallOptions.LocalServingCertDir + "/tls.key", + } + err := admission.Serve(ctx, options) + Expect(err).NotTo(HaveOccurred()) + }() + + By("waiting for webhook server to become ready") + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + + By("creating testing namespace") + _, err = clientset.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testingNamespace, + }, + }, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + threads.Wait() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = Describe("Create secrets", func() { + var err error + + It("should generate correct secret values", func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + StringData: map[string]string{ + "regularKey": "regularValue", + "uuidKey": "%generate:uuid", + "base32UuidKey": "%generate:uuid:encoding=base32", + "base64UuidKey": "%generate:uuid:encoding=base64", + "base64UrlUuidKey": "%generate:uuid:encoding=base64_url", + "base64RawUuidKey": "%generate:uuid:encoding=base64_raw", + "base64RawUrlUuidKey": "%generate:uuid:encoding=base64_raw_url", + "simplePasswordKey": "%generate:password", + "complexPasswordKey": "%generate:password:length=10;num_digits=1;num_symbols=1;symbols=_", + "base32PasswordKey": "%generate:password:length=100;encoding=base32", + "base64PasswordKey": "%generate:password:length=100;encoding=base64", + "base64UrlPasswordKey": "%generate:password:length=100;encoding=base64_url", + "base64RawPasswordKey": "%generate:password:length=100;encoding=base64_raw", + "base64RawUrlPasswordKey": "%generate:password:length=100;encoding=base64_raw_url", + }, + } + + secret, err = clientset.CoreV1().Secrets(testingNamespace).Create(ctx, secret, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKeyWithValue("regularKey", []byte("regularValue"))) + + Expect(secret.Data).To(HaveKey("uuidKey")) + _, err = uuid.ParseBytes(secret.Data["uuidKey"]) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base32UuidKey")) + Expect(secret.Data["base32UuidKey"]).NotTo(BeEmpty()) + _, err = base32.StdEncoding.DecodeString(string(secret.Data["base32UuidKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64UuidKey")) + Expect(secret.Data["base64UuidKey"]).NotTo(BeEmpty()) + _, err = base64.StdEncoding.DecodeString(string(secret.Data["base64UuidKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64UrlUuidKey")) + Expect(secret.Data["base64UrlUuidKey"]).NotTo(BeEmpty()) + _, err = base64.URLEncoding.DecodeString(string(secret.Data["base64UrlUuidKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64RawUuidKey")) + Expect(secret.Data["base64RawUuidKey"]).NotTo(BeEmpty()) + _, err = base64.RawStdEncoding.DecodeString(string(secret.Data["base64RawUuidKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64RawUrlUuidKey")) + Expect(secret.Data["base64RawUrlUuidKey"]).NotTo(BeEmpty()) + _, err = base64.RawURLEncoding.DecodeString(string(secret.Data["base64RawUrlUuidKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("simplePasswordKey")) + Expect(secret.Data["simplePasswordKey"]).To(HaveLen(32)) + + Expect(secret.Data).To(HaveKey("complexPasswordKey")) + Expect(secret.Data["complexPasswordKey"]).To(HaveLen(10)) + + Expect(secret.Data).To(HaveKey("base32PasswordKey")) + Expect(secret.Data["base32PasswordKey"]).NotTo(BeEmpty()) + _, err = base32.StdEncoding.DecodeString(string(secret.Data["base32PasswordKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64PasswordKey")) + Expect(secret.Data["base64PasswordKey"]).NotTo(BeEmpty()) + _, err = base64.StdEncoding.DecodeString(string(secret.Data["base64PasswordKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64UrlPasswordKey")) + Expect(secret.Data["base64UrlPasswordKey"]).NotTo(BeEmpty()) + _, err = base64.URLEncoding.DecodeString(string(secret.Data["base64UrlPasswordKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64RawPasswordKey")) + Expect(secret.Data["base64RawPasswordKey"]).NotTo(BeEmpty()) + _, err = base64.RawStdEncoding.DecodeString(string(secret.Data["xbase64RawPasswordKey"])) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(HaveKey("base64RawUrlPasswordKey")) + Expect(secret.Data["base64RawUrlPasswordKey"]).NotTo(BeEmpty()) + _, err = base64.RawURLEncoding.DecodeString(string(secret.Data["base64RawUrlPasswordKey"])) + Expect(err).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("Update secrets", func() { + var specifiedSecret, createdSecret *corev1.Secret + var err error + + BeforeEach(func() { + specifiedSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Data: map[string][]byte{ + "regularKey": []byte("regularValue"), + "uuidKey": []byte("%generate:uuid"), + "passwordKey": []byte("%generate:password"), + }, + } + + createdSecret, err = clientset.CoreV1().Secrets(testingNamespace).Create(ctx, specifiedSecret, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + specifiedSecret.GenerateName = "" + specifiedSecret.Name = createdSecret.Name + specifiedSecret.ResourceVersion = createdSecret.ResourceVersion + }) + + It("should update secrets correctly", func() { + specifiedSecret.Data["regularKey"] = []byte("anotherValue") + expectedData := maps.Clone(createdSecret.Data) + expectedData["regularKey"] = specifiedSecret.Data["regularKey"] + + updatedSecret, err := clientset.CoreV1().Secrets(testingNamespace).Update(ctx, specifiedSecret, metav1.UpdateOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(updatedSecret.Data).To(Equal(expectedData)) + }) +}) + +// assemble mutatingwebhookconfiguration descriptor +func buildMutatingWebhookConfiguration() *admissionv1.MutatingWebhookConfiguration { + return &admissionv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mutate", + }, + Webhooks: []admissionv1.MutatingWebhook{{ + Name: "mutate-secrets.test.local", + AdmissionReviewVersions: []string{"v1"}, + ClientConfig: admissionv1.WebhookClientConfig{ + Service: &admissionv1.ServiceReference{ + Path: &[]string{"/core/v1/secret/mutate"}[0], + }, + }, + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Update, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"secrets"}, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": testingNamespace, + }, + }, + SideEffects: &[]admissionv1.SideEffectClass{admissionv1.SideEffectClassNone}[0], + }}, + } +} diff --git a/internal/webhook/utils.go b/internal/webhook/utils.go new file mode 100644 index 0000000..a37f07f --- /dev/null +++ b/internal/webhook/utils.go @@ -0,0 +1,20 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package webhook + +import "strings" + +func normalizeSymbols(symbols string) string { + var l []rune + var i = 0 + for _, r := range Symbols { + if strings.ContainsRune(symbols, r) { + l = append(l, r) + i++ + } + } + return string(l) +} diff --git a/internal/webhook/utils_test.go b/internal/webhook/utils_test.go new file mode 100644 index 0000000..5cb15b2 --- /dev/null +++ b/internal/webhook/utils_test.go @@ -0,0 +1,20 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and secret-generator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package webhook + +import ( + "testing" +) + +func TestNormalizeSymbols(t *testing.T) { + if symbols := normalizeSymbols(Symbols); symbols != Symbols { + t.Error("normalizeSymbols: got invalid symbols") + } + + if symbols := normalizeSymbols("_+-_+-"); symbols != "-_+" { + t.Error("normalizeSymbols: got invalid symbols") + } +} diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go new file mode 100644 index 0000000..912cda9 --- /dev/null +++ b/internal/webhook/webhook.go @@ -0,0 +1,21 @@ +package webhook + +import ( + "context" + + corev1 "k8s.io/api/core/v1" +) + +type SecretWebhook struct{} + +func NewSecretWebhook() *SecretWebhook { + return &SecretWebhook{} +} + +func (w *SecretWebhook) MutateCreate(ctx context.Context, secret *corev1.Secret) error { + return handleCreateSecret(secret) +} + +func (w *SecretWebhook) MutateUpdate(ctx context.Context, oldSecret *corev1.Secret, newSecret *corev1.Secret) error { + return handleUpdateSecret(newSecret, oldSecret) +}