From 79b262fa0d3bf232b5f8bd76dad982f0b8e23b4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:12:27 +0000 Subject: [PATCH 1/3] chore: bump k8s.io/klog/v2 from 2.100.1 to 2.120.1 in /test/e2eprovider Bumps [k8s.io/klog/v2](https://github.com/kubernetes/klog) from 2.100.1 to 2.120.1. - [Release notes](https://github.com/kubernetes/klog/releases) - [Changelog](https://github.com/kubernetes/klog/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes/klog/compare/v2.100.1...v2.120.1) --- updated-dependencies: - dependency-name: k8s.io/klog/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- test/e2eprovider/go.mod | 4 ++-- test/e2eprovider/go.sum | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/e2eprovider/go.mod b/test/e2eprovider/go.mod index 0f6712396..b8e07c579 100644 --- a/test/e2eprovider/go.mod +++ b/test/e2eprovider/go.mod @@ -7,7 +7,7 @@ replace sigs.k8s.io/secrets-store-csi-driver => ../.. require ( github.com/google/go-cmp v0.5.9 google.golang.org/grpc v1.60.1 - k8s.io/klog/v2 v2.100.1 + k8s.io/klog/v2 v2.120.1 monis.app/mlog v0.0.4 sigs.k8s.io/secrets-store-csi-driver v0.0.0-00010101000000-000000000000 sigs.k8s.io/yaml v1.4.0 @@ -18,7 +18,7 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/test/e2eprovider/go.sum b/test/e2eprovider/go.sum index e3267437c..d46a52f2b 100644 --- a/test/e2eprovider/go.sum +++ b/test/e2eprovider/go.sum @@ -10,10 +10,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t 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/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/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/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -154,8 +153,8 @@ k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc= k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM= k8s.io/component-base v0.26.4 h1:Bg2xzyXNKL3eAuiTEu3XE198d6z22ENgFgGQv2GGOUk= k8s.io/component-base v0.26.4/go.mod h1:lTuWL1Xz/a4e80gmIC3YZG2JCO4xNwtKWHJWeJmsq20= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= monis.app/mlog v0.0.4 h1:YEzh5sguG4ApywaRWnBU+mGP6SA4WxOqiJ36u+KtoeE= From 3c94dec6cf7af4d08858b23416853413cb754bcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:02:07 +0000 Subject: [PATCH 2/3] chore: bump actions/dependency-review-action from 3.1.5 to 4.0.0 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3.1.5 to 4.0.0. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/c74b580d73376b7750d3d2a50bfb8adc2c937507...4901385134134e04cec5fbe5ddfe3b2c5bd5d976) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 988c44e6c..7ad9aa61d 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@c74b580d73376b7750d3d2a50bfb8adc2c937507 # v3.1.5 + uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 From 3f27b06dc67f6517417cdbc020db245a6c479c29 Mon Sep 17 00:00:00 2001 From: mandrea Date: Mon, 11 Mar 2024 17:57:44 +0000 Subject: [PATCH 3/3] secret sync controller implementation --- secret-sync-controller/.dockerignore | 4 + secret-sync-controller/.gitignore | 26 + secret-sync-controller/Dockerfile | 34 + secret-sync-controller/Makefile | 181 ++++++ secret-sync-controller/PROJECT | 19 + secret-sync-controller/README.md | 130 ++++ .../api/v1alpha1/groupversion_info.go | 36 ++ .../api/v1alpha1/secretsync_types.go | 242 +++++++ .../api/v1alpha1/zz_generated.deepcopy.go | 177 +++++ secret-sync-controller/charts/index.yaml | 15 + .../charts/secretsync-0.0.1.tgz | Bin 0 -> 11275 bytes .../charts/secretsync/.helmignore | 23 + .../charts/secretsync/Chart.yaml | 6 + .../charts/secretsync/README.md | 38 ++ .../secret-sync.x-k8s.io_secretsyncs.yaml | 268 ++++++++ ...re.csi.x-k8s.io_secretproviderclasses.yaml | 177 +++++ .../VAPB_create_update_secrets_type.yaml | 9 + ...VAPB_create_update_token_request_deny.yaml | 9 + .../templates/VAPB_delete_secrets.yaml | 9 + .../VAPB_update_owners_check_old_object.yaml | 9 + .../VAPB_update_secrets_based_on_label.yaml | 9 + ...alidate_annotation_format_secret_sync.yaml | 9 + ...APB_validate_label_format_secret_sync.yaml | 9 + .../templates/VAPB_validate_token_config.yaml | 9 + .../charts/secretsync/templates/_helpers.tpl | 95 +++ .../create_update_secrets_types.yaml | 26 + .../create_update_token_request_deny.yaml | 24 + .../secretsync/templates/delete_secrets.yaml | 21 + .../templates/leader_election_role.yaml | 46 ++ .../leader_election_role_binding.yaml | 19 + .../secretsync/templates/namespace.yaml | 12 + .../templates/secret-sync-controller.yaml | 179 +++++ .../templates/secretsync_editor_role.yaml | 31 + .../templates/secretsync_viewer_role.yaml | 27 + .../update_owners_check_old_object.yaml | 30 + .../update_secrets_based_on_label.yaml | 28 + ...alidate_annotation_format_secret_sync.yaml | 26 + .../validate_label_format_secret_sync.yaml | 26 + .../templates/validate_token_config.yaml | 29 + .../charts/secretsync/values.yaml | 92 +++ .../secret-sync.x-k8s.io_secretsyncs.yaml | 268 ++++++++ ...re.csi.x-k8s.io_secretproviderclasses.yaml | 177 +++++ .../config/crd/kustomization.yaml | 22 + .../config/default/kustomization.yaml | 76 +++ .../config/default/manager_config_patch.yaml | 10 + .../config/manager/kustomization.yaml | 8 + .../config/manager/manager.yaml | 140 ++++ .../config/manifests/kustomization.yaml | 26 + .../config/rbac/kustomization.yaml | 12 + .../config/rbac/leader_election_role.yaml | 44 ++ .../rbac/leader_election_role_binding.yaml | 19 + secret-sync-controller/config/rbac/role.yaml | 52 ++ .../config/rbac/role_binding.yaml | 19 + .../config/rbac/secretsync_editor_role.yaml | 31 + .../config/rbac/secretsync_viewer_role.yaml | 27 + .../config/rbac/service_account.yaml | 12 + .../config_allow_secret_types.yaml | 22 + .../config_deny_secret_types.yaml | 15 + .../config_ssc.yaml | 19 + .../create_update_secrets_types.yaml | 34 + .../delete_secrets.yaml | 28 + .../kustomization.yaml | 15 + .../update_owners_check_old_object.yaml | 50 ++ .../update_secrets_based_on_label.yaml | 37 ++ .../validate_annotation_format.yaml | 24 + .../validate_label_format.yaml | 26 + .../validate_token_config.yaml | 36 ++ .../controllers/conditions.go | 177 +++++ .../controllers/secretsync_controller.go | 467 ++++++++++++++ .../controllers/suite_test.go | 80 +++ secret-sync-controller/go.mod | 88 +++ secret-sync-controller/go.sum | 257 ++++++++ .../hack/boilerplate.go.txt | 15 + secret-sync-controller/main.go | 144 +++++ .../pkg/fakeprovider/fake_provider_client.go | 172 +++++ secret-sync-controller/pkg/k8s/token.go | 105 +++ .../pkg/k8s/token/token_manager.go | 213 ++++++ .../pkg/k8s/token/token_manager_test.go | 610 ++++++++++++++++++ secret-sync-controller/pkg/k8s/token_test.go | 87 +++ .../pkg/metrics/exporter.go | 27 + .../pkg/metrics/prometheus_exporter.go | 52 ++ .../pkg/provider/provider_client.go | 309 +++++++++ .../pkg/util/secretutil/secret.go | 234 +++++++ test/bats/e2e-provider-ssc.bats | 70 ++ .../e2e_provider_v1_secretproviderclass.yaml | 12 + .../e2e_provider_v1alpha1_secretsync.yaml | 12 + 86 files changed, 6539 insertions(+) create mode 100644 secret-sync-controller/.dockerignore create mode 100644 secret-sync-controller/.gitignore create mode 100644 secret-sync-controller/Dockerfile create mode 100644 secret-sync-controller/Makefile create mode 100644 secret-sync-controller/PROJECT create mode 100644 secret-sync-controller/README.md create mode 100644 secret-sync-controller/api/v1alpha1/groupversion_info.go create mode 100644 secret-sync-controller/api/v1alpha1/secretsync_types.go create mode 100644 secret-sync-controller/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 secret-sync-controller/charts/index.yaml create mode 100644 secret-sync-controller/charts/secretsync-0.0.1.tgz create mode 100644 secret-sync-controller/charts/secretsync/.helmignore create mode 100644 secret-sync-controller/charts/secretsync/Chart.yaml create mode 100644 secret-sync-controller/charts/secretsync/README.md create mode 100644 secret-sync-controller/charts/secretsync/crds/secret-sync.x-k8s.io_secretsyncs.yaml create mode 100644 secret-sync-controller/charts/secretsync/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_create_update_secrets_type.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_create_update_token_request_deny.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_delete_secrets.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_update_owners_check_old_object.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_update_secrets_based_on_label.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_validate_annotation_format_secret_sync.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_validate_label_format_secret_sync.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/VAPB_validate_token_config.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/_helpers.tpl create mode 100644 secret-sync-controller/charts/secretsync/templates/create_update_secrets_types.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/create_update_token_request_deny.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/delete_secrets.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/leader_election_role.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/leader_election_role_binding.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/namespace.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/secret-sync-controller.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/secretsync_editor_role.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/secretsync_viewer_role.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/update_owners_check_old_object.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/update_secrets_based_on_label.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/validate_annotation_format_secret_sync.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/validate_label_format_secret_sync.yaml create mode 100644 secret-sync-controller/charts/secretsync/templates/validate_token_config.yaml create mode 100644 secret-sync-controller/charts/secretsync/values.yaml create mode 100644 secret-sync-controller/config/crd/bases/secret-sync.x-k8s.io_secretsyncs.yaml create mode 100644 secret-sync-controller/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml create mode 100644 secret-sync-controller/config/crd/kustomization.yaml create mode 100644 secret-sync-controller/config/default/kustomization.yaml create mode 100644 secret-sync-controller/config/default/manager_config_patch.yaml create mode 100644 secret-sync-controller/config/manager/kustomization.yaml create mode 100644 secret-sync-controller/config/manager/manager.yaml create mode 100644 secret-sync-controller/config/manifests/kustomization.yaml create mode 100644 secret-sync-controller/config/rbac/kustomization.yaml create mode 100644 secret-sync-controller/config/rbac/leader_election_role.yaml create mode 100644 secret-sync-controller/config/rbac/leader_election_role_binding.yaml create mode 100644 secret-sync-controller/config/rbac/role.yaml create mode 100644 secret-sync-controller/config/rbac/role_binding.yaml create mode 100644 secret-sync-controller/config/rbac/secretsync_editor_role.yaml create mode 100644 secret-sync-controller/config/rbac/secretsync_viewer_role.yaml create mode 100644 secret-sync-controller/config/rbac/service_account.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/config_allow_secret_types.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/config_deny_secret_types.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/config_ssc.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/create_update_secrets_types.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/delete_secrets.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/kustomization.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/update_owners_check_old_object.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/update_secrets_based_on_label.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/validate_annotation_format.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/validate_label_format.yaml create mode 100644 secret-sync-controller/config/validatingadmissionpolicies/validate_token_config.yaml create mode 100644 secret-sync-controller/controllers/conditions.go create mode 100644 secret-sync-controller/controllers/secretsync_controller.go create mode 100644 secret-sync-controller/controllers/suite_test.go create mode 100644 secret-sync-controller/go.mod create mode 100644 secret-sync-controller/go.sum create mode 100644 secret-sync-controller/hack/boilerplate.go.txt create mode 100644 secret-sync-controller/main.go create mode 100644 secret-sync-controller/pkg/fakeprovider/fake_provider_client.go create mode 100644 secret-sync-controller/pkg/k8s/token.go create mode 100644 secret-sync-controller/pkg/k8s/token/token_manager.go create mode 100644 secret-sync-controller/pkg/k8s/token/token_manager_test.go create mode 100644 secret-sync-controller/pkg/k8s/token_test.go create mode 100644 secret-sync-controller/pkg/metrics/exporter.go create mode 100644 secret-sync-controller/pkg/metrics/prometheus_exporter.go create mode 100644 secret-sync-controller/pkg/provider/provider_client.go create mode 100644 secret-sync-controller/pkg/util/secretutil/secret.go create mode 100644 test/bats/e2e-provider-ssc.bats create mode 100644 test/bats/tests/e2e_provider_ssc/e2e_provider_v1_secretproviderclass.yaml create mode 100644 test/bats/tests/e2e_provider_ssc/e2e_provider_v1alpha1_secretsync.yaml diff --git a/secret-sync-controller/.dockerignore b/secret-sync-controller/.dockerignore new file mode 100644 index 000000000..0f046820f --- /dev/null +++ b/secret-sync-controller/.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/secret-sync-controller/.gitignore b/secret-sync-controller/.gitignore new file mode 100644 index 000000000..e917e5cef --- /dev/null +++ b/secret-sync-controller/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/secret-sync-controller/Dockerfile b/secret-sync-controller/Dockerfile new file mode 100644 index 000000000..9671ed915 --- /dev/null +++ b/secret-sync-controller/Dockerfile @@ -0,0 +1,34 @@ +# Build the manager binary +FROM golang:1.21@sha256:7026fb72cfa9cc112e4d1bf4b35a15cac61a413d0252d06615808e7c987b33a7 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY main.go main.go +COPY api/ api/ +COPY controllers/ controllers/ +COPY pkg/ pkg/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +# TODO: LDFLAGS +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:latest +WORKDIR / +COPY --from=builder /workspace/manager . + +ENTRYPOINT ["/manager"] diff --git a/secret-sync-controller/Makefile b/secret-sync-controller/Makefile new file mode 100644 index 000000000..d04b93af9 --- /dev/null +++ b/secret-sync-controller/Makefile @@ -0,0 +1,181 @@ +# Update this value when you upgrade the version of your project. +VERSION ?= v0.0.1 + +# Set the Operator SDK version to use. By default, what is installed on the system is used. +# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. +OPERATOR_SDK_VERSION ?= v1.30.0 + +# Image URL to use all building/pushing image targets +IMG ?= secret-sync-controller:$(VERSION) +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.26.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.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: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: test ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v4.5.7 +CONTROLLER_TOOLS_VERSION ?= v0.11.1 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.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 + +.PHONY: operator-sdk +OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk +operator-sdk: ## Download operator-sdk locally if necessary. +ifeq (,$(wildcard $(OPERATOR_SDK))) +ifeq (, $(shell which operator-sdk 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPERATOR_SDK)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ + chmod +x $(OPERATOR_SDK) ;\ + } +else +OPERATOR_SDK = $(shell which operator-sdk) +endif +endif + diff --git a/secret-sync-controller/PROJECT b/secret-sync-controller/PROJECT new file mode 100644 index 000000000..6f2a38a34 --- /dev/null +++ b/secret-sync-controller/PROJECT @@ -0,0 +1,19 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: secret-sync.x-k8s.io +layout: +- go.kubebuilder.io/v3 +projectName: secret-sync-controller +repo: sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: secret-sync.x-k8s.io + kind: SecretSync + path: sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/secret-sync-controller/README.md b/secret-sync-controller/README.md new file mode 100644 index 000000000..42eb25b76 --- /dev/null +++ b/secret-sync-controller/README.md @@ -0,0 +1,130 @@ +# Secret Sync Controller + +This is a Kubernetes controller that watches for changes to a custom resource and syncs the secrets from external secrets-store as Kubernetes secret. This feature is useful for syncing secrets across multiple namespaces and making sure that the secrets are available when the cluster is offline. + +> NOTE: This code is in experimental stage and is not recommended for production use. The implementation is currently a [pull request](https://github.com/kubernetes-sigs/secrets-store-csi-driver/pull/1466) in the secrets-store-csi-driver repository and is being reviewed by the community. + +## Description + +This proposal is a diversion from the current design of the Secrets Store CSI driver. Based on feedback, some of the users want the CSI driver to sync the secret store objects as Kubernetes secrets without the mount instead of the tight coupling between the mount and the sync as it is [today](https://secrets-store-csi-driver.sigs.k8s.io/topics/sync-as-kubernetes-secret). + +To support this, we could extract the sync controller from the CSI driver and have it as a standalone deployment. Just syncing as Kubernetes secrets is a cluster-scope operation and doesn’t require the controller or CSI pods to be run on all nodes. The controller would need to watch for Create/Update events for the Secret Sync (SS) and create the Kubernetes secrets by making an RPC call to the provider. + +For more information, see the [design proposal](https://docs.google.com/document/d/1Ylwpg-YXNw6kC9-kdHNYD3ZKskj9TTIopwIxz5VUOW4/edit#heading=h.n3xa8h2b1inm). + +## Getting Started + +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +The helm chart for the controller has validating admission policies that are available for k8s 1.27 and later. If you are using an older version of k8s, you may need to disable the validating admission policies by setting the `validatingAdmissionPolicies.applyPolicies` parameter to `false` in the `secret-sync-controller/secretsync/values.yaml` file, but this is not recommended. We recommend using a k8s version 1.27 or later. + +Before you begin, ensure the [following](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#before-you-begin). + +If you're creating a kind cluster, here's a sample config: + +```bash +cat < +cat < +cat <. + // The following label prefix is reserved: secrets-store.sync.x-k8s.io/. + // Creation fails if the label is specified in the SecretSync object with a different value. + // On secret update, if the validation admission policy is set, the controller will check if the label + // secrets-store.sync.x-k8s.io/secretsync= is present. If the label is not present, + // controller fails to update the secret. + // +kubebuilder:validation:XValidation:message="Labels should have < 63 characters for both keys and values.",rule="(self.all(x, x.size() < 63 && self[x].size() < 63) == true)" + // +kubebuilder:validation:XValidation:message="Labels should not contain secrets-store.sync.x-k8s.io. This key is reserved for the controller.",rule="(self.all(x, x.startsWith('secrets-store.sync.x-k8s.io') == false))" + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // annotations contains key-value pairs representing annotations associated with the Kubernetes secret object. + // The following annotation prefix is reserved: secrets-store.sync.x-k8s.io/. + // Creation fails if the annotation key is specified in the SecretSync object by the user. + // +kubebuilder:validation:XValidation:message="Annotations should have < 253 characters for both keys and values.",rule="(self.all(x, x.size() < 253 && self[x].size() < 253) == true)" + // +kubebuilder:validation:XValidation:message="Annotations should not contain secrets-store.sync.x-k8s.io. This key is reserved for the controller.",rule="(self.all(x, x.startsWith('secrets-store.sync.x-k8s.io') == false))" + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// SecretSyncSpec defines the desired state for synchronizing secret. +type SecretSyncSpec struct { + // secretSyncControllerName specifies the name of the secret sync controller used to synchronize + // the secret. + // +optional + // +kubebuilder:default:="" + SecretSyncControllerName string `json:"secretSyncControllerName"` + + // secretProviderClassName specifies the name of the secret provider class used to pass information to + // access the secret store. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + // +kubebuilder:validation:Required + SecretProviderClassName string `json:"secretProviderClassName"` + + // serviceAccountName specifies the name of the service account used to access the secret store. + // The audience field in the service account token must be passed as parameter in the controller configuration. + // The audience is used when requesting a token from the API server for the service account; the supported + // audiences are defined by each provider. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + // +kubebuilder:validation:Required + ServiceAccountName string `json:"serviceAccountName"` + + // secretObject specifies the configuration for the synchronized Kubernetes secret object. + // +kubebuilder:validation:Required + SecretObject SecretObject `json:"secretObject"` + + // forceSynchronization can be used to force the secret synchronization. The secret synchronization is + // triggered, by changing the value in this field. + // This field is not used to resolve synchronization conflicts. + // It is not related with the force query parameter in the Apply operation. + // https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=^[A-Za-z0-9]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)? + // +optional + ForceSynchronization string `json:"forceSynchronization,omitempty"` +} + +// SecretSyncStatus defines the observed state of the secret synchronization process. +type SecretSyncStatus struct { + // syncHash contains the hash of the secret object data, data from the SecretProviderClass (e.g. UID, + // and metadata.generation), and similar data from the SecretSync. This hash is used to + // determine if the secret changed. + // The hash is calculated using the HMAC (Hash-based Message Authentication Code) algorithm, using bcrypt + // hashing, with the SecretsSync's UID as the key. + // The secret is updated if: + // 1. the hash is different + // 2. the lastSuccessfulSyncTime indicates a rotation is required + // - the rotation poll interval is passed as a parameter in the controller configuration + // 3. the SecretUpdateStatus is 'Failed' + // +optional + SyncHash string `json:"syncHash,omitempty"` + + // lastSuccessfulSyncTime represents the last time the secret was retrieved from the Provider and updated. + // +optional + LastSuccessfulSyncTime *metav1.Time `json:"lastSuccessfulSyncTime,omitempty"` + + // conditions represent the status of the secret create and update processes. + // The status is set to True if the secret was created or updated successfully. + // The status is set to False if the secret create or update failed. + // The status is set to Unknown if the secret patch failed due to an unknown error. + // The following conditions are used: + // - Type: Create + // - Status: True + // Reason: CreateSucceeded + // Message: The secret was created successfully. + // - Status: False + // Reason: ProviderError + // Message: The secret create failed due to a provider error: errorCode, check the logs or the events for more information. + // - Status: False + // Reason: InvalidClusterSecretLabelError + // Message: The secret create failed because a label reserved for the controller is applied on the secret. + // - Status: False + // Reason: InvalidClusterSecretAnnotationError + // Message: The secret create failed because an annotation reserved for the controller is applied on the secret. + // - Status: False + // Reason: UnknownError + // Message: Secret patch failed due to unknown error, check the logs or the events for more information. + // - Status: False + // Reason: ValidatingAdmissionPolicyCheckFailed + // Message: The secret update failed because the validating admission policy check failed. + // - Status: False + // Reason: UserInputValidationFailed + // Message: The secret update failed because the user input validation failed. (e.g. if a secret type is invalid). + // - Status: False + // Reason: ControllerSPCError + // Message: The secret update failed because the controller failed to get the secret provider class, or the SPC is misconfigured. + // - Status: False + // Reason: ControllerInternalError + // Message: The secret update failed due to an internal error, check the logs or the events for more information. + // - Type: Update + // - Status: True + // Reason: NoValueChange + // Message: The secret was updated successfully at the end of the poll interval and no value change was detected. + // - Status: True + // Reason: ValueChangeOrForceUpdateDetected + // Message: The secret was updated successfully:a value change or a force update was detected. + // - Status: False + // Reason: ValidatingAdmissionPolicyCheckFailed + // Message: The secret update failed because the validating admission policy check failed. + // - Status: False + // Reason: InvalidClusterSecretLabelError + // Message: The secret update failed because a label reserved for the controller is applied on the secret. + // - Status: False + // Reason: InvalidClusterSecretAnnotationError + // Message: The secret update failed because an annotation reserved for the controller is applied on the secret. + // - Status: False + // Reason: ProviderError + // Message: The secret update failed due to a provider error: errorCode, check the logs or the events for more information. + // - Status: False + // Reason: UserInputValidationFailed + // Message: The secret update failed because the user input validation failed. (e.g. if a secret type is invalid). + // - Status: False + // Reason: ControllerSPCError + // Message: The secret update failed because the controller failed to get the secret provider class, or the SPC is misconfigured. + // - Status: False + // Reason: ControllerInternalError + // Message: The secret update failed due to an internal error, check the logs or the events for more information. + // - Status: False + // Reason: UnknownError + // Message: Secret patch failed due to unknown error, check the logs or the events for more information. + + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +kubebuilder:validation:MaxItems=16 + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// +kubebuilder:object:root=true +// +genclient +// +kubebuilder:object:generate:=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// SecretSync represents the desired state and observed state of the secret synchronization process. +// The SecretSync name is used to as the secret object created by the controller. +type SecretSync struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SecretSyncSpec `json:"spec,omitempty"` + Status SecretSyncStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// SecretSyncList contains a list of SecretSync resources. +type SecretSyncList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SecretSync `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SecretSync{}, &SecretSyncList{}) +} diff --git a/secret-sync-controller/api/v1alpha1/zz_generated.deepcopy.go b/secret-sync-controller/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..7f4629c19 --- /dev/null +++ b/secret-sync-controller/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,177 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretObject) DeepCopyInto(out *SecretObject) { + *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make([]SecretObjectData, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretObject. +func (in *SecretObject) DeepCopy() *SecretObject { + if in == nil { + return nil + } + out := new(SecretObject) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretObjectData) DeepCopyInto(out *SecretObjectData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretObjectData. +func (in *SecretObjectData) DeepCopy() *SecretObjectData { + if in == nil { + return nil + } + out := new(SecretObjectData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSync) DeepCopyInto(out *SecretSync) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSync. +func (in *SecretSync) DeepCopy() *SecretSync { + if in == nil { + return nil + } + out := new(SecretSync) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SecretSync) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSyncList) DeepCopyInto(out *SecretSyncList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SecretSync, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSyncList. +func (in *SecretSyncList) DeepCopy() *SecretSyncList { + if in == nil { + return nil + } + out := new(SecretSyncList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SecretSyncList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSyncSpec) DeepCopyInto(out *SecretSyncSpec) { + *out = *in + in.SecretObject.DeepCopyInto(&out.SecretObject) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSyncSpec. +func (in *SecretSyncSpec) DeepCopy() *SecretSyncSpec { + if in == nil { + return nil + } + out := new(SecretSyncSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSyncStatus) DeepCopyInto(out *SecretSyncStatus) { + *out = *in + if in.LastSuccessfulSyncTime != nil { + in, out := &in.LastSuccessfulSyncTime, &out.LastSuccessfulSyncTime + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSyncStatus. +func (in *SecretSyncStatus) DeepCopy() *SecretSyncStatus { + if in == nil { + return nil + } + out := new(SecretSyncStatus) + in.DeepCopyInto(out) + return out +} diff --git a/secret-sync-controller/charts/index.yaml b/secret-sync-controller/charts/index.yaml new file mode 100644 index 000000000..14617e8ad --- /dev/null +++ b/secret-sync-controller/charts/index.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +entries: + secretsync: + - apiVersion: v2 + appVersion: v0.0.1 + created: "2024-03-17T22:45:30.413428184Z" + description: A Helm chart to install the SecretSync Controller and its associated + resources inside a Kubernetes cluster. + digest: 3742923c5c58f97ed1ade0bdaae2069cdc1193a95900f93c6acd32f68c742b01 + kubeVersion: '>=1.27.0' + name: secretsync + urls: + - secretsync-0.0.1.tgz + version: 0.0.1 +generated: "2024-03-17T22:45:30.411818527Z" diff --git a/secret-sync-controller/charts/secretsync-0.0.1.tgz b/secret-sync-controller/charts/secretsync-0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..59d69308ab7331d8b8f8d7c6547a1638525651c8 GIT binary patch literal 11275 zcmV+mEcDYKiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PKBxciT3W`24L;aevBeWp_+UvSTN!Sx=A0Nv1i?!zW3*XEw>& z5D7`BDS`z+#cGoM?7xEtN$@37wxcwz@JC{iARZUEPuzRKvB$9B%h2ne&JYvs63v6Z z+-?~R27{yf_tpP{!Jzp6;9&p$UxxP&9~>S%7!DuY|I1)_aPZ*ZFEF^}aa5L=3&j31 zxN%>_$$cXQMdTOEIHBPP7W*9(Md{ChJ8*{`A9Ih9NT^3A@FNc9;K`i=f&vM-KtTXv zhT*l^Nw1fo2d6X?j0OQ_fI=UL;D9)%9zg>89RP+ojhTnJY=HO}5d0L6F$=N49K0ar z0yDRB8IN%~n%+N-hwlCZchKvEXpTpa?cB~HKjg0s({HI*_`g6wjQJNJ01fzmf4Dy^ z;{VaXV2l5oDEHtIo}f4opqC#elv$|dcJ9HO8R0-UAo%ym^JmV4vN;k0`!FE^mK7dh zFF*{#0x^Qd0p^l4j4?!rbL;~N1%)Mz83;U&0wgeZJ6=j6URc=d$UdFyo&Dz=g=mVI z!hRkhul^&xQipZ$neOQ_g z^C-Zb4x<7|0(eG);Ato@TcF@+_!@gO^!W((hev|}?*w%E3ua@=iCB){5IUU&3W$#c z38yFioN%t2ARfVd)B!+I6fDyRAXtn$0GBBxe1!vyct!%dLwEn7JLt+!gMfa(zNWcv zmXU5y&aC?~LchhhoBYh0a|!K_5htF5VlhiAj42gdFci7@stS5F&G^*2#LT1NgiL?s zw4vH$TJxUsS$-^q5d^&3=>YgRB-L{*G>6%Oc-TRnN8?a9W(FNHM^ijfd?%8tL)mf! zh@m;+xNr7)aT6umV!m`3o)RwD(#3i|xE`JN)$@pU0HQbu)FLiN@O1KmiZg~e4n+q* zpy>#@iYawF9ge-25wVoq5nl;8pd$9|frmngI&^^5%o{P(4dW~E|f_fqz@DINl*2VyoAMC^ zB#f^l=?E}0gNKd-G?G6l8^Pl%!UgYi&}2eFh3Opt+3rd5K!HO-iZJXyiWv!~ue}-e zvOS8_ecBTedk8-t6&J#IT|x=^Od(5-&#}mJv>$fi>b6 zR6Gr>H+rj$tS1gE2oo_190`yBi%w^b1tT6mqfCt8;o#w6rxP){AUCnM_c)S6XoMa2gn56U$pYKLSl7u;wm_wKX*QU1QL2d>|^Lw%i=CdnmI`g z4P9{I^XJYb3H=fLQq$q`pCt52IF*bD`A8tiuQau(>B`X?*3lc*iBv1jSU1z{@CbXd z4gLJZ+AT>LHGvKdPdp`>y@_nuVm?S!!FN#94^~afkym4 z92D^X;K9M+7XLR)aW-)73%Tdmx@5j_-|qsY{dUT!T!HL7#{3z z@qZKL4&lGvus6c?Tod0sZnr7*_-`0L{Sact&%GJ;F3)M;pVRTL*jwwIuo3@{it_)# z!TtMN{NF^mL-=oawBE3e+OQ4{e7#?9L>Ah#{EbqJM*L3{|1sj&Kd0e&fW|mjOB`s{ z|4Z`!@cwX%|C=ax5C7HvybW}>8~R4EySdckznLyRN9jS%xjy?fG~rww5Vs%%jrf0X zRKWkkE&gw)+#&o=Hms9v*c*|7`u6S+HE4nViVj{A^;K!+|3&@({^0)B|8FDZF5>^q zC4kDN?hw{zr3LSZx_nm*?5Hln&@K~9&v==a=0S1FU%8%bT=I36x zo$vdK5;)-F2?^^dLDF_tkq!qm{=e2fn&~dP#`_WY+a4JuyK%7u7Vr_ z!UZ4>2s}EE0xYmK_lkARCeJfh+-HP~Y|ekR-oWS>-1H5ZKuATtf_$gF+|@(tqox z?{{-RD*+H5>8VQyg^acv;>hH3*P=puv^*RaN@bm2>%!yM$h6AZlBz zHx%`W9!#%Sv3pKu*t?Vf6*CMpNO#W+arFpaMc5PA*8_k{ylhVlXkh%ge?(Ezf$}H;Dh;<#lm;=vb9C12yuu=8A%~u7u%|#=5xk*E z7U6*BkyvB52|KFeG#!FPw{kT!=`w`2avVjfR`|Szu&JI<8eEdPa?9@2%#un4YL`M! zQoKS}pFej1RKpoO&kz%`Zo+{KJ(U8mo3zDUtQ^^QKJ38<#fkod_jbEkXK76}D$_^R z;Ew@CkzD@Dx5Z%<>8$+wS4u+Y!lzHrt!||1qdHC}x7Zvx97jk#@PP{^1(*q=b3h8t z?Kh6B{f53)EfjZ)*%R$IOFFg21ver?w%%!9<)x@v0G>9HU&U&`@7Z8416u?55f+%u zC9EW&LlFTJbg5X7e3q$>kScvz*^ z#7Wz8P(&QF&1?bw8&$Rckfl=puXp{wmLFh){=fgAbpF3T9B%dhjg&vE{qKg3|CzmS zf=CcEoWztpK_ut^&xFopqK&dUq@ZYHC2-*r{1#JzHA_z_ zO7cm*cFnM+Q>hZ~&$CC5^ zeD+BGl~v3_6*)P46|w$g6p;wDwQ zog~(ga~0p&eHjMJBnTHi5HT~BCN1*$QvFuaqwL+-;Xw)}W;kx4#mDyGd)2t8bZ?2` z*atzO2Y&-2@$V!eaJuz(;cw7Obapj7S>4pOj83|qvKGk-Ju_^pa?*5am(!o5KV5h; z!&Z#43hSm4I+|7iWuVDb3N9NqaxR&PPK^`+C*d@}R&EARDB z@tM*U^_w}0{v@1_aAhJ*`5LWfff}4M!qo_F$k?`}RjK6vw-@@SN(28N9v&9_e}@ka zw()vr-CvymO>?-C~9=Dz=ud@xnHZkM|irt?QJ%}(fgjeNl^eO1&hh+KP+@TE{BhN*#m{CT4FCO)1n*NFgI7hy< zuYsmxC@Llz)TCGLv6>1hnV3q}s7K}`BXd@Kj^Zau1k1e4GXLRObf?2rdf>5UKGXec00LzY4TaByGYG$ zf!WwRpJJi@4+z(Pe2`pCf1&`a{?yXG`ZG%3YjRoc-lLTHB(yhJc|7V^^2?8<3KV#O zLs973YF{_{wUN6;X-)sn6Nk_B`9Kr>-#HkK`HAs7vXMS4>Z#L1j2>t4B zxAZ=PPaCpN1N;h4bQ)q!?N)4n4uHklufvNH@9&ZEUux1DGjDOJFoh)aaVTJ44*hGR z%$+Kg^gq*gZY3Ff1N|Qy4T|~SkM@tY_MeTE-2D#;m4A_}{~9#jqQ6-}=36Df8Fw%V z-d8c3bPL~rp0`Ok-JX)yP~xw={+0M&LzAyd3|8a+aQL8{|LwuyR{q;asl@+6P*x)m zz+nkRYmjB*a$6#)*x^+-APH@e{nq2x08l=kK0wEDoOMkzr#ghMWT9Cp(3z2HL2ey; zK`SM>RZ9l$x6_op7r4IXqO##qbNSWxILSh`YDS!7M)v~5+nOqBy3ISR)q-dldYkH| z&jk{3O>dc5#n@VIwp-U%I=?Vs$w-}z9=49Uk|LQ{+Gf4>HmOnz{7wdQP5scOFY8d1 zTT~B=`wT@9&$iSf90hba#|5^wa-@~KtQF1PtjOF>Uai}aeg>J_G=VgV1WlVaInL$Q zy)EY4^qB?Tw$pEeEeuBi@em)uumkC>RiDI_!iDT{B zhkCEb667XS*Nh2*t!N>E6%_80S=pjpv3gkcZ8)o1vg%bggmQP})U=XRs%|2^DJUI^ zabcJW*a>i$0jhXi%`ObGW~v}t{JAR&I9>fmfm_}Q3v(J&2FKB&!0!7 z*8)x96PSyUz}2^Mo}~9OX8l%Nm{V#jyPV4YPG6&w{BpoaugY=sEilLFRWG?(+O^Ug zk~>Mr;BlJ};#?r5YY#>?Ji{muGdYSeRvB9yKlFghRQ9s^7W4t&F!E8gh4dUA0KEr;;)(??%o1Tpc~fWhmt zlhZE-h|C}at?_el69kPY6FE#XF*8C$+!}RC)AV&)&3hRjVpLF%1|xWLdUj)iP))!p zF04yR1^F!0wut9&VD}Y}1r9OiXA;@%HYHU4g++cjBejZsO(1^Hze?^V8(NPg=O8F} zgafopFK8Sd+K|JHP(Qo8abWp7BcH52H2Kx?x+7aLuwgbZ71AF1_D1S|*~OBdD0keH z=A~EmT1#oEh95tsUBD+OwUuL-#kft6$aeMM&8Q~E;B20;D%a`m_ z+bM2h|65`Il@HIcFDc>8JiyjJ{~rzqTl?=uO5Xo#LYYFaEAC!MbgqzwxI961Cv*%MddWLP-ES%mtCkKx6mIWv(P5; z3av`HK*J=!60$@%Dnwad1h{fG{?^i4b)NlhP2buFR{LblTCWCSR6U z^AklR%H3w1sA7~>w&#^&Qbw7pJy7!P~3%5@tsSZF_io z_1*^T3drvMkb`XzUTx)>Drh8`IN{SL*r}*mw+Hj`PrH@ct+zm7p!(J)3qBwTBEde; z(3S$EWP?H=p_(>mzUjix+P0E!Lx~K6Edfmt30LGkD+n;9yX3BGbJ~o(X;bhk+MS;2 zHnvu$`m$R7yYbcEsx--e`$xt6ZwLGLxANac%AJz`u44hZQ=*^J>DQF{nyoTL?y1c_ zYe+ZhgOy^39>gDB!qYQfSGc>@^c(87ljhR8UrCVDGjA!w+1qNh_*PnmHj>zroSrR{q;axl{6=j!)j605+Te z8ric};;hJzQ!jhstBB~vO`Kd=B+4@>C8dQ? z@a^`|!2buO{Qtv)qiy`>X39M{LqcGtlmF}0hYvFx!Z;>@FNr~fyh}92-0j?hH#5S) z+}g~6&u|dHG@xV6zeqUU1BL@6$O5Y{XX`l%{mwlI@l=zEorvKHxx&V0^Z)I-p!^PK zsJ@g#f(SDRNQm8z`{?!gYe5(IuReo0@L5N4KQ%rS1mH|`-Wd-6 zCCS*ipy&Fx(t`g{6WIq!@VDbN!<+pq3pYH*& zX*|cFa66q(*$h;^Xs9ip;1T8?BaxcPri$!UXXsI??fTz$J~?em`SfMgOcptGV$w9OC;6lR^7X0EkR7jKIhR2=kyJ7$Yw2CnPAFORGJLc&EpLZh}V zJyjK@W30xN)Wf~*Sn7JcXq~F2YUpZ)sp7I4P}vN%tv63A75xH1Z+U_%W~g&7HZ882 zq0;1DX|*j;Wh*|^Gfb56bhm0fFwy#Md475qx7nj=CMy%jU07}YNI$@DG4U>gRlH0E zKJx5BRc@Q(i2%ZNYA+H_TTJ{DTd z844CR(1IJyuoUTcVHQ(|iP1^wK`v%Uw3w}IaE(bygzLVH&~GvB?m;)7OJR&S@f;M3 znS3{jo_;8 z>tAo}C1eY;($+x7m;)VS6RzG~YKFyBiZwe?Pfb+@EAQeLX1<@`Ldvd)<1F`Do-d6S z19#vKFRCV$rBJM$)KfJfSyAKfnk#VKNj;sspyG^Sjzh6F>RU4uXxc`K;1Tx7gm`BC z1e!{5BIekImkZ38()dKWzViKt8Ra<5ok@5mb@Vt~pfri*MHRq4!9}-w1G=5f@GK4j zy}5Yp8lEtQ)J`PXVkW33g$m?(*r-^HERc{JcuKfnYZL3-4Bvk2O6jbt?%ADs4Ex>B zt%cuI5=`K~lJ%Y#Tew`c2r)rW5G;Y3PZN<%`8iGEAPAM@oSYKF&Ib|%Fh@k`1!^G~ zRyQ&#@p88s2Dh??$!Hp8nV+HjsP*h_fUmbQ!$N+}y5>Gf5~(UG{S04;f_C;r6qiqM z@lo^ZGe#C9z*GE~dq|rqjQ`jkcql?+5)eT!AHhfP8I4Bp_T<^K_wX4$8%~%WKDHq^ zN##V0?+0W~3_8{`LQT`IIZ@ri2o48>Ih>yTya&VmhtJ91lOd?Ipytid{pTb%4Xt74 z^1iQ7J?-r9g2EUB@_l^>7E%A&G^J^VA3^eZPXq<&$Pj5KRs2WLP4cD6<;%@bdt^1O zp-Ei7smWzCEL;uHtw;g8*K{vHx@GNr`77z`3+ktCXL5^1)Ca^lnTE=S_T{FVUr5SF z&<#i!UuEl7|Gn0<(>(sG>VJYaR>J=c&hYI=!HPaVVJPrSD489|HyC6)&C~)L*2Uw)j|J3Pp-pnw^ zw(*xvCYZq_z*j_38ZUJK<{PG(ihzlx2EJV+KE^`w(!r zAdDJty~1-mA15w>QGmR}j4hR3Mk7X$z>-_D)XuNX(vv_5k?KTWSqGD`YRlDvAahRe zwa-<)m13R$dCbpU{g*nopW6u_ui*u_`uy+U{?TB+nE&%|oBv}orF>fw!B+x@%8jg? z%~PFFy)t6hBdlm6p<&KhuXItJ#ciiJRAPp5F2GDRo~BzHxWl14?0`-`OlbIq%rO^e z9*rQ3gPt!Dd}6NY`@LaJ|)+|x0~Y>_^q6Fb@bESF7%vN+;%;_-29w-RX%JR3)_%=hyO4uJPlC$a z$e~k#X($+=%SkIRYFiWG8MLy{>8#KRpfHKEQ1@d>o@z{N9t{_m31HZx(~$h0ww)fi zDz|D;Xj9!X6QV$~KII?-bF>786?o#%YR??kW+aDKqVscv&9Qei~ zM*S_@lN&7=t=#>=0vEfjXk3cy0r4abJQYbZ%I7X|rPE$9a`wN)m@P9Uz<7*QGbZMAZ2JW)3GlcGR&||6V%IQg?H7^$dqoF&n}D;X&!O zasC^@|JKCsz5m|&hIDA|H$7z6r%Y`P1Ya}qz^et?3rl?^q3ssbcEJDlGbia1>2rK9(Mgb3p~ zf?v~ZE-e7()Fb)*Tnj6Svo&pIo?%wP0PS@Oir;JDXH+o!JtrJ`@BlM+mn{g^j{mD)P#JLW;O+rO^M6(N% zYns(eRxW|!um<h6*{5x|;TcH7%=q=3c3N!=?Hm zVkY~Qd#<@fGn{&4Zxm>(iX==b4p&sGl7QUxM4+8_{hha};_mMEe_hd6BfnRb5SwE0 z6JEC7DZTWQWTz-{Sc8VW-s+{bP#SU5(YO+=PtKk~Dxj_!EwmGHEe0y(TrKB0DD)NW zMN{mgDsy5wT@3rx{92bgzFEvUfL{lD$08!NRnpC~Qb=7P`OHc9R8j95(CG#cW2m}| zoa7V`E}S`v94X|~$xf?>Ty9T~V2+}S`o&wo?eYa>d2>?PN{lyStdr)~mEhCl9ITFW zB#JWeDew4xlctcFdyoIic6JWY9RH(k2yZI7 z`?4xZI;9nw$tu)>B>BHjCW=Z5&G=mloZ5D_mvot_9(DUAae_47a%y;3>=iSJD32rM z?VOuUO{>>lEGQWulVT9Sg{K8zN|nL9+_OilK`&@_L1I!y**Yb|)l{^*3 zSv?E5k&<^*t>Yctf{K@)V-K)9bs_Q5{(HTP zcJWo}LtT6!g4}(``9;Z5ih0aU&Z6vkLy3ka{ZQh^xl&ol2&4o;^>Vh5<{IU=m2A3R zo^q5}T=7`8S7QRLq#zm?iQc8uE_H!+8*6CktHSoNL#b-_Orm3frk zHdL%pxykD+lDgi8-IYrTeIjF*6sH(Oyp`PMMBni4y?W1-4dB^p_(Rou;; z@_@>Rx#}X-cE`D@YR)KG^8Bbr{yOiET(x?GqgP0=WtUnDYi=UlG=Em6oBCKN$y)AO z8e%{a|3xwnFJ`QK;Ge+_r%oRd0TOp67aYei8BSD^0*P596gc#afaeodCxFLX4gyf9 ztqNd5vox_#W=U7Cn41B_1$aUk;43tb0=!q*?z>QnU{5GD%c#fp5&C_2m3Q#bi)zj? z=mg5b0jOg!+T#g`G&o$@*iRw-5LS&+^^X`H!MpCs0+9fX1N^RAGv4kQqf@3MW8QVO zSIfKZBRplu$Nsyn?CNjo;`ry7P4Q27d8|4}AHNn12|QgM&*l5dPGsGHa1|bRJV#Ob z;5mxY2D995@b>*23$z%z>9fjaFMj1T9KGvibKj$Lxkn?heAk8C=tl3lYJAD_WHO_7 zUD^A)YB@DxRM2re8NKU{mjd&>;U2@$o@7_YS+DQ97joVEeV{5*RBa*-pR3wzvh)Xt zi#H5~oFoo)txhz`pCxu&`38i1V3^hlfP&J{l2(k#UEkiQ_p)TVp_-&I^cx**taA|6 zH#)-*hdyS(QfK~13B(MAQ|!B-HFT7ENLdXu~vc>C~=@Lb@&BB?w`iyQC&yf@fkT1uXy$gNfp%tg`kkVH)rm?1E z**d0YGD{W45rRSxGl4o_Sls?#|H0A2>%t;oT>rwMaaL&smhxvs@5t<$vT0gp#r`w& zJ==L7kg{2Ux-1dmNWw`{_vDItWer=a6OvNtUPz z6Ap5z=|t^UZRBHEvsL58xQeYpp0(n1-c8s+LUDAzZqtyE%;Wh81}nFV)q0TiWT;{IYwa`(~>!=X{K%GDM;F4Y8G5dvn$T75lmymP$)3= zCGphyiatun2t3N%DjR4A*p72@s>VfA(VZ3qXE@kjvBR1U*-8g&@2v#t=X)bNnf&?t z-5=@>&>Bfwl|-MFyaKG*XCm$eI`3z0y|o)pY6Q?I7-CCHce1=&Cvn|@2fz-;7~GKh zxXdGVHN{6J`b#DLNt`t6Pdos~T&n&kG$(ZoPw^EHk@#dt#Jz^qQfyKRA~leH|+c-XsoQFGH_d*(s?x z<&TP@VP55_wU9~)kYT|HUL>lHte-kGsWf{gs-X0Fv$C+tfVvF59}%x`C6P^+~iN%sdq|Vkf2y~d!0;yy2 z%=}%|~T_z0!|G2>Z}g8iU)4U|9f<}KPcY+`(S??|GSa$ z2aEsRSMk4gly7mr)o;-1sNc$(wXwfBE~efVgMf22YNCM)W7brl5Eq=QvsQqxZtmKl z!Ffh)th)L;>kzPR8oHdEJxzaYV|2GMy4x7t6-#jYF}gVvZV1**BXr87Eh+V9NLl`v zvTD)R6M3bgoSWQfDT*>RcqXK%MtrN(T$67aw4!pasXtO!Jd1CbkL?O@?c;7cn5a3B z2>bBU!)oJAPM52y(sXv%7+AWVJ9?}5fgC98s8BkA%ZCNu@s*~T6;ES)qW}e%B_*3#7KVIu52B-=_`jDl=Q8bmEoYw~8Q2_(QdGSM6|YKQ03j<8pTM zAvNxj<}i^}ff`>LH%!C?os_rDbaJovz>L;0ul*uV=&xM~kUj3OFISIMI$e2oR zH968&1KMgpTMcNd0cB;Y0c|y)ss^?iP{a7P8qih)+G;>s4d{=e0o7B4@$!sTc;mF= zNdJ@{R}8TAxRRorgt5BM5ryi6eL?&f1*@aKn&=#SFn?fF;B+dL z=jYI7)20{7wZ)zAf9bE#FM}{{a91|NqJ6tL6YU0RZr? BCV~I} literal 0 HcmV?d00001 diff --git a/secret-sync-controller/charts/secretsync/.helmignore b/secret-sync-controller/charts/secretsync/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/secret-sync-controller/charts/secretsync/Chart.yaml b/secret-sync-controller/charts/secretsync/Chart.yaml new file mode 100644 index 000000000..37bcdeec0 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: secretsync +version: 0.0.1 +appVersion: v0.0.1 +kubeVersion: ">=1.27.0" +description: A Helm chart to install the SecretSync Controller and its associated resources inside a Kubernetes cluster. diff --git a/secret-sync-controller/charts/secretsync/README.md b/secret-sync-controller/charts/secretsync/README.md new file mode 100644 index 000000000..c4471faec --- /dev/null +++ b/secret-sync-controller/charts/secretsync/README.md @@ -0,0 +1,38 @@ +# Deploying the SecretSync Controller +You can deploy the SecretSync Controller using Helm. This guide provides instructions for deploying the SecretSync Controller using Helm. + +You can use the following commands to deploy the SecretSync Controller using Helm: +```sh +helm install -f values secret-sync-controller charts/secretsync +``` + +# Configuration and Parameters +You can customize the installation by modifying values in the values.yaml file or by passing parameters to the helm install command using the --set key=value[,key=value] argument. + +| Parameter Name | Description | Default Value | +|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| +| `providerContainer` | The container for the Secret Sync Provider | `[- name: provider-aws-installer ...]` | +| `controllerName` | The name of the SecretSync Controller. | `secret-sync-controller-manager` | +| `namespace` | The namespace to deploy the chart to. | `secret-sync-controller-system` | +| `tokenRequestAudience` | The audience for the token request. | `[]` | +| `rotationPollInterval` | How quickly the SecretSync Controller checks or updates the secret it is managing. | `21600s` (6 hours) | +| `logVerbosity` | The log level. | `5` | +| `validatingAdmissionPolicies.applyPolicies` | Determines whether the SecretSync Controller should apply policies. | `true` | +| `validatingAdmissionPolicies.allowedSecretTypes` | The types of secrets that the SecretSync Controller should allow. | `["Opaque", "kubernetes.io/basic-auth", "bootstrap.kubernetes.io/token", "kubernetes.io/dockerconfigjson", "kubernetes.io/dockercfg", "kubernetes.io/ssh-auth", "kubernetes.io/tls"]` | +| `validatingAdmissionPolicies.deniedSecretTypes`| The types of secrets that the SecretSync Controller should deny. | `["kubernetes.io/service-account-token"]` | +| `image.repository` | The image repository of the SecretSync Controller. | `aramase/secrets-sync-controller:v0.0.1` | +| `image.pullPolicy` | Image pull policy. | `IfNotPresent` | +| `image.tag` | The specific image tag to use. Overrides the image tag whose default is the chart's `appVersion`. | `""` | +| `imagePullSecrets` | Array of image pull secrets for accessing private registries. | `[{"name": "regcred"}]` | +| `nameOverride` | A string to partially override `secretsync.fullname` template (will maintain the release name). | `""` | +| `fullnameOverride` | A string to fully override `secretsync.fullname` template. | `""` | +| `securityContext` | Security context for the SecretSync Controller. | `{ allowPrivilegeEscalation: false, capabilities: { drop: [ALL] } }` | +| `resources` | The resource request/limits for the SecretSync Controller image. | `limits: 500m CPU, 128Mi; requests: 10m CPU, 64Mi` | +| `podAnnotations` | Annotations to be added to pods. | `{ kubectl.kubernetes.io/default-container: "manager" }` | +| `podLabels` | Labels to be added to pods. | `{ control-plane: "controller-manager", secrets-store.io/system: "true" }` | +| `nodeSelector` | Node labels for pod assignment. | `{ kubernetes.io/os: "linux" }` | +| `tolerations` | Tolerations for pod assignment. | `[{ operator: "Exists" }]` | +| `affinity` | Affinity settings for pod assignment. | `key: type; operator: NotIn; values: [virtual-kubelet]` | + + +These parameters offer flexibility in configuring and deploying the SecretSync Controller according to specific requirements in your Kubernetes environment. Remember to replace values appropriately or use the --set flag when installing the chart via Helm. diff --git a/secret-sync-controller/charts/secretsync/crds/secret-sync.x-k8s.io_secretsyncs.yaml b/secret-sync-controller/charts/secretsync/crds/secret-sync.x-k8s.io_secretsyncs.yaml new file mode 100644 index 000000000..285ee6f33 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/crds/secret-sync.x-k8s.io_secretsyncs.yaml @@ -0,0 +1,268 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: secretsyncs.secret-sync.x-k8s.io +spec: + group: secret-sync.x-k8s.io + names: + kind: SecretSync + listKind: SecretSyncList + plural: secretsyncs + singular: secretsync + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: SecretSync represents the desired state and observed state of + the secret synchronization process. The SecretSync name is used to as the + secret object created by the controller. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretSyncSpec defines the desired state for synchronizing + secret. + properties: + forceSynchronization: + description: forceSynchronization can be used to force the secret + synchronization. The secret synchronization is triggered, by changing + the value in this field. This field is not used to resolve synchronization + conflicts. It is not related with the force query parameter in the + Apply operation. https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts + maxLength: 253 + pattern: ^[A-Za-z0-9]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)? + type: string + secretObject: + description: secretObject specifies the configuration for the synchronized + Kubernetes secret object. + properties: + annotations: + additionalProperties: + type: string + description: 'annotations contains key-value pairs representing + annotations associated with the Kubernetes secret object. The + following annotation prefix is reserved: secrets-store.sync.x-k8s.io/. + Creation fails if the annotation key is specified in the SecretSync + object by the user.' + type: object + x-kubernetes-validations: + - message: Annotations should have < 253 characters for both keys + and values. + rule: (self.all(x, x.size() < 253 && self[x].size() < 253) == + true) + - message: Annotations should not contain secrets-store.sync.x-k8s.io. + This key is reserved for the controller. + rule: (self.all(x, x.startsWith('secrets-store.sync.x-k8s.io') + == false)) + data: + description: data is a slice of SecretObjectData containing secret + data source from the Secret Provider Class and the corresponding + data field key used in the Kubernetes secret object. + items: + description: SecretObjectData defines the desired state of synchronized + data within a Kubernetes secret object. + properties: + sourcePath: + description: sourcePath is the data source value of the + secret defined in the Secret Provider Class. This matches + the path of a file in the MountResponse returned from + the provider. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9.]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)?(\/([0-9]+))*$ + type: string + targetKey: + description: 'targetKey is the key in the Kubernetes secret''s + data field as described in the Kubernetes API reference: + https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/' + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9.]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)?(\/([0-9]+))*$ + type: string + required: + - sourcePath + - targetKey + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - targetKey + x-kubernetes-list-type: map + labels: + additionalProperties: + type: string + description: 'labels contains key-value pairs representing labels + associated with the Kubernetes secret object. The labels are + used to identify the secret object created by the controller. + On secret creation, the following label is added: secrets-store.sync.x-k8s.io/secretsync=. + The following label prefix is reserved: secrets-store.sync.x-k8s.io/. + Creation fails if the label is specified in the SecretSync object + with a different value. On secret update, if the validation + admission policy is set, the controller will check if the label + secrets-store.sync.x-k8s.io/secretsync= is + present. If the label is not present, controller fails to update + the secret.' + type: object + x-kubernetes-validations: + - message: Labels should have < 63 characters for both keys and + values. + rule: (self.all(x, x.size() < 63 && self[x].size() < 63) == + true) + - message: Labels should not contain secrets-store.sync.x-k8s.io. + This key is reserved for the controller. + rule: (self.all(x, x.startsWith('secrets-store.sync.x-k8s.io') + == false)) + type: + description: type specifies the type of the Kubernetes secret + object, e.g. "Opaque";"kubernetes.io/basic-auth";"kubernetes.io/ssh-auth";"kubernetes.io/tls" + The controller must have permission to create secrets of the + specified type. + maxLength: 253 + minLength: 1 + type: string + required: + - data + - type + type: object + secretProviderClassName: + description: secretProviderClassName specifies the name of the secret + provider class used to pass information to access the secret store. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + secretSyncControllerName: + default: "" + description: secretSyncControllerName specifies the name of the secret + sync controller used to synchronize the secret. + type: string + serviceAccountName: + description: serviceAccountName specifies the name of the service + account used to access the secret store. The audience field in the + service account token must be passed as parameter in the controller + configuration. The audience is used when requesting a token from + the API server for the service account; the supported audiences + are defined by each provider. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - secretObject + - secretProviderClassName + - serviceAccountName + type: object + status: + description: SecretSyncStatus defines the observed state of the secret + synchronization process. + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastSuccessfulSyncTime: + description: lastSuccessfulSyncTime represents the last time the secret + was retrieved from the Provider and updated. + format: date-time + type: string + syncHash: + description: 'syncHash contains the hash of the secret object data, + data from the SecretProviderClass (e.g. UID, and metadata.generation), + and similar data from the SecretSync. This hash is used to determine + if the secret changed. The hash is calculated using the HMAC (Hash-based + Message Authentication Code) algorithm, using bcrypt hashing, with + the SecretsSync''s UID as the key. The secret is updated if: 1. + the hash is different 2. the lastSuccessfulSyncTime indicates a + rotation is required - the rotation poll interval is passed as a + parameter in the controller configuration 3. the SecretUpdateStatus + is ''Failed''' + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/secret-sync-controller/charts/secretsync/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/secret-sync-controller/charts/secretsync/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml new file mode 100644 index 000000000..fcf63c668 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -0,0 +1,177 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.1 + name: secretproviderclasses.secrets-store.csi.x-k8s.io +spec: + group: secrets-store.csi.x-k8s.io + names: + kind: SecretProviderClass + listKind: SecretProviderClassList + plural: secretproviderclasses + singular: secretproviderclass + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: SecretProviderClass is the Schema for the secretproviderclasses + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretProviderClassSpec defines the desired state of SecretProviderClass + properties: + parameters: + additionalProperties: + type: string + description: Configuration for specific provider + type: object + provider: + description: Configuration for provider name + type: string + secretObjects: + items: + description: SecretObject defines the desired state of synced K8s + secret objects + properties: + annotations: + additionalProperties: + type: string + description: annotations of k8s secret object + type: object + data: + items: + description: SecretObjectData defines the desired state of + synced K8s secret object data + properties: + key: + description: data field to populate + type: string + objectName: + description: name of the object to sync + type: string + type: object + type: array + labels: + additionalProperties: + type: string + description: labels of K8s secret object + type: object + secretName: + description: name of the K8s secret object + type: string + type: + description: type of K8s secret object + type: string + type: object + type: array + type: object + status: + description: SecretProviderClassStatus defines the observed state of SecretProviderClass + type: object + type: object + served: true + storage: true + - deprecated: true + deprecationWarning: secrets-store.csi.x-k8s.io/v1alpha1 is deprecated. Use secrets-store.csi.x-k8s.io/v1 + instead. + name: v1alpha1 + schema: + openAPIV3Schema: + description: SecretProviderClass is the Schema for the secretproviderclasses + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretProviderClassSpec defines the desired state of SecretProviderClass + properties: + parameters: + additionalProperties: + type: string + description: Configuration for specific provider + type: object + provider: + description: Configuration for provider name + type: string + secretObjects: + items: + description: SecretObject defines the desired state of synced K8s + secret objects + properties: + annotations: + additionalProperties: + type: string + description: annotations of k8s secret object + type: object + data: + items: + description: SecretObjectData defines the desired state of + synced K8s secret object data + properties: + key: + description: data field to populate + type: string + objectName: + description: name of the object to sync + type: string + type: object + type: array + labels: + additionalProperties: + type: string + description: labels of K8s secret object + type: object + secretName: + description: name of the K8s secret object + type: string + type: + description: type of K8s secret object + type: string + type: object + type: array + type: object + status: + description: SecretProviderClassStatus defines the observed state of SecretProviderClass + properties: + byPod: + items: + description: ByPodStatus defines the state of SecretProviderClass + as seen by an individual controller + properties: + id: + description: id of the pod that wrote the status + type: string + namespace: + description: namespace of the pod that wrote the status + type: string + type: object + type: array + type: object + type: object + served: true + storage: false diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_create_update_secrets_type.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_create_update_secrets_type.yaml new file mode 100644 index 000000000..0ba8ecec9 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_create_update_secrets_type.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-create-update-policy-binding" +spec: + policyName: "secret-sync-controller-create-update-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_create_update_token_request_deny.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_create_update_token_request_deny.yaml new file mode 100644 index 000000000..546b930d9 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_create_update_token_request_deny.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-create-update-token-deny-policy-binding" +spec: + policyName: "secret-sync-controller-create-update-token-deny-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_delete_secrets.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_delete_secrets.yaml new file mode 100644 index 000000000..60a317bc6 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_delete_secrets.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-delete-policy-binding" +spec: + policyName: "secret-sync-controller-delete-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_update_owners_check_old_object.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_update_owners_check_old_object.yaml new file mode 100644 index 000000000..9215e6f2d --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_update_owners_check_old_object.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-update-owners-check-oldobject-policy-binding" +spec: + policyName: "secret-sync-controller-update-owners-check-oldobject-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_update_secrets_based_on_label.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_update_secrets_based_on_label.yaml new file mode 100644 index 000000000..90e4e44bf --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_update_secrets_based_on_label.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-update-label-policy-binding" +spec: + policyName: "secret-sync-controller-update-label-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_validate_annotation_format_secret_sync.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_validate_annotation_format_secret_sync.yaml new file mode 100644 index 000000000..4b3bbb216 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_validate_annotation_format_secret_sync.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-validate-annotation-policy-binding" +spec: + policyName: "secret-sync-controller-validate-annotation-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_validate_label_format_secret_sync.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_validate_label_format_secret_sync.yaml new file mode 100644 index 000000000..c04f655f3 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_validate_label_format_secret_sync.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-validate-label-policy-binding" +spec: + policyName: "secret-sync-controller-validate-label-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/VAPB_validate_token_config.yaml b/secret-sync-controller/charts/secretsync/templates/VAPB_validate_token_config.yaml new file mode 100644 index 000000000..7255915a7 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/VAPB_validate_token_config.yaml @@ -0,0 +1,9 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "secret-sync-controller-validate-token-policy-binding" +spec: + policyName: "secret-sync-controller-validate-token-policy" + validationActions: [Deny] +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/_helpers.tpl b/secret-sync-controller/charts/secretsync/templates/_helpers.tpl new file mode 100644 index 000000000..19d85e993 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/_helpers.tpl @@ -0,0 +1,95 @@ +{{/* +Generate subjects for role binding and cluster role binding. +*/}} +{{- define "secret-sync-controller.subjects" -}} +- kind: ServiceAccount + name: "secret-sync-controller-manager" + namespace: {{ .Values.namespace }} +{{- end }} + +{{/* Generate match condition expression */}} +{{- define "chartname.matchConditionExpression" -}} +{{- printf "request.userInfo.username == 'system:serviceaccount:%s:%s'" .Values.namespace .Values.controllerName -}} +{{- end -}} + +{{/* +Generate allowed secret types list as a complete expression. +*/}} +{{- define "chartname.secretTypesList" -}} +{{- $secretTypes := . -}} +{{- if not $secretTypes -}} +false +{{- else -}} +(object.type in [{{ range $index, $type := $secretTypes }}{{ if $index }}, {{ end }}"{{ $type }}"{{ end }}]) +{{- end -}} +{{- end -}} + +{{/* Define a constant value for labelKey */}} +{{- define "secret-sync-controller.labelKey" -}} +secrets-store.sync.x-k8s.io +{{- end -}} + +{{/* Define a constant value for labelValue */}} +{{- define "secret-sync-controller.labelValue" -}} +'' +{{- end -}} + +{{/* +Check if the old secret has the expected label key. +*/}} +{{- define "secret-sync-controller.oldSecretHasExpectedLabelKey" -}} +variables.oldSecretHasLabels && ('{{ include "secret-sync-controller.labelKey" . }}' in oldObject.metadata.labels) ? true : false +{{- end -}} + +{{/* +Check if the old secret has the expected label value. +*/}} +{{- define "secret-sync-controller.oldSecretHasExpectedLabelValue" -}} +{{ include "secret-sync-controller.labelValue" . }} == oldObject.metadata.labels['{{ include "secret-sync-controller.labelKey" . }}'] ? true : false +{{- end -}} + + +{{/* +Generate token audience comparison expression. +Returns 'false' if tokenRequestAudience list is empty. +*/}} +{{- define "secret-sync-controller.tokenAudienceComparison" -}} +{{- $tokenAudiences := .Values.tokenRequestAudience -}} +{{- if not $tokenAudiences -}} +false +{{- else -}} +{{- $audienceExpressions := list -}} +{{- range $index, $audience := $tokenAudiences }} + {{- $expressionPart := printf "object.spec.audiences.exists(w, w == '%s')" $audience.audience -}} + {{- $audienceExpressions = append $audienceExpressions $expressionPart -}} +{{- end -}} +{{- join " || " $audienceExpressions -}} +{{- end -}} +{{- end -}} + +{{/* +Generate a comma-separated string from a list. +*/}} +{{- define "secret-sync-controller.listToString" -}} +{{- $tokenRequests := .Values.tokenRequestAudience -}} +{{- $audiences := list -}} +{{- range $index, $request := $tokenRequests }} + {{- $audiences = append $audiences $request.audience -}} +{{- end -}} +{{- join ", " $audiences -}} +{{- end -}} + +{{/* +Determine the api version for the validating admission policies. +*/}} +{{- define "secret-sync-controller.admissionApiVersion" -}} +{{- if semverCompare "~1.27.0" .Values.validatingAdmissionPolicies.kubernetesReleaseVersion -}} +apiVersion: admissionregistration.k8s.io/v1alpha1 +{{- else if semverCompare "~1.28.0" .Values.validatingAdmissionPolicies.kubernetesReleaseVersion -}} +apiVersion: admissionregistration.k8s.io/v1beta1 +{{- else if semverCompare "~1.29.0" .Values.validatingAdmissionPolicies.kubernetesReleaseVersion -}} +apiVersion: admissionregistration.k8s.io/v1 +{{- else -}} +apiVersion: unsupported-validating-admission-api-version +{{- end }} +{{- end -}} \ No newline at end of file diff --git a/secret-sync-controller/charts/secretsync/templates/create_update_secrets_types.yaml b/secret-sync-controller/charts/secretsync/templates/create_update_secrets_types.yaml new file mode 100644 index 000000000..59b9a64bc --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/create_update_secrets_types.yaml @@ -0,0 +1,26 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-create-update-policy" +spec: + failurePolicy: Fail + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: {{ include "chartname.matchConditionExpression" . | quote }} + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["secrets"] + variables: + - name: hasOneSecretSyncOwner + expression: "(size(object.metadata.ownerReferences) == 1 && object.metadata.ownerReferences.all(o, o.kind == 'SecretSync' && o.apiVersion.startsWith('secret-sync.x-k8s.io/') && o.name == object.metadata.name))" + - name: allowedSecretTypes + expression: {{ include "chartname.secretTypesList" .Values.validatingAdmissionPolicies.allowedSecretTypes | quote }} + validations: + - expression: "variables.allowedSecretTypes == true && variables.hasOneSecretSyncOwner == true" + message: "Only secrets with types defined in the allowedSecretTypes are allowed." + messageExpression: "'secret-sync-controller has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace. The controller can only create or update secrets in the allowed types list with a single secretsync owner.'" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/create_update_token_request_deny.yaml b/secret-sync-controller/charts/secretsync/templates/create_update_token_request_deny.yaml new file mode 100644 index 000000000..65d7e7c67 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/create_update_token_request_deny.yaml @@ -0,0 +1,24 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-create-update-token-deny-policy" +spec: + failurePolicy: Fail + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: {{ include "chartname.matchConditionExpression" . | quote }} + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["secrets"] + variables: + - name: deniedSecretTypes + expression: {{ include "chartname.secretTypesList" .Values.validatingAdmissionPolicies.deniedSecretTypes | quote }} + validations: + - expression: "variables.deniedSecretTypes == false" + message: "Only secrets with types defined in the allowedSecretTypes are allowed." + messageExpression: "'secret-sync-controller has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace. The controller is not allowed to create or update secrets with this type.'" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/delete_secrets.yaml b/secret-sync-controller/charts/secretsync/templates/delete_secrets.yaml new file mode 100644 index 000000000..6a5d60c87 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/delete_secrets.yaml @@ -0,0 +1,21 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-delete-policy" +spec: + failurePolicy: Fail + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: {{ include "chartname.matchConditionExpression" . | quote }} + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["DELETE"] + resources: ["secrets"] + validations: + - expression: "request.operation == 'delete'" + message: "The controller is not allowed to delete secrets." + messageExpression: "'secret-sync-controller has failed to ' + string(request.operation) + ' secrets in the ' + string(request.namespace) + ' namespace. The controller is not allowed to delete secrets.'" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/leader_election_role.yaml b/secret-sync-controller/charts/secretsync/templates/leader_election_role.yaml new file mode 100644 index 000000000..8a56dbbbd --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/leader_election_role.yaml @@ -0,0 +1,46 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + secrets-store.io/system: "true" + name: secret-sync-controller-leader-election-role + namespace: {{ .Values.namespace }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/secret-sync-controller/charts/secretsync/templates/leader_election_role_binding.yaml b/secret-sync-controller/charts/secretsync/templates/leader_election_role_binding.yaml new file mode 100644 index 000000000..94beb52a1 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/leader_election_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + secrets-store.io/system: "true" + name: secret-sync-controller-leader-election-rolebinding + namespace: {{ .Values.namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: secret-sync-controller-leader-election-role +subjects: + {{- include "secret-sync-controller.subjects" . | nindent 2 }} diff --git a/secret-sync-controller/charts/secretsync/templates/namespace.yaml b/secret-sync-controller/charts/secretsync/templates/namespace.yaml new file mode 100644 index 000000000..a060c9ddc --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/namespace.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/instance: system + app.kubernetes.io/name: namespace + app.kubernetes.io/part-of: secret-sync-controller + control-plane: controller-manager + secrets-store.io/system: "true" + name: {{ .Values.namespace }} \ No newline at end of file diff --git a/secret-sync-controller/charts/secretsync/templates/secret-sync-controller.yaml b/secret-sync-controller/charts/secretsync/templates/secret-sync-controller.yaml new file mode 100644 index 000000000..2be070d41 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/secret-sync-controller.yaml @@ -0,0 +1,179 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kuberentes.io/instance: controller-manager + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: secret-sync-controller + secrets-store.io/system: "true" + name: "secret-sync-controller-manager" + namespace: {{ .Values.namespace }} + annotations: + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: secret-sync-controller-manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - secrets-store.csi.x-k8s.io + resources: + - secretproviderclasses + verbs: + - get + - list + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs + verbs: + - get + - list + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/instance: manager-rolebinding + + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: secret-sync-controller + secrets-store.io/system: "true" + name: secret-sync-controller-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: secret-sync-controller-manager-role +subjects: + {{- include "secret-sync-controller.subjects" . | nindent 2 }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: secret-sync-controller-manager + namespace: {{ .Values.namespace }} + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: secret-sync-controller + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/instance: controller-manager + secrets-store.io/system: "true" +spec: + selector: + matchLabels: + control-plane: controller-manager + secrets-store.io/system: "true" + replicas: 1 + template: + metadata: + annotations: + {{- toYaml .Values.podAnnotations | nindent 8 }} + labels: + {{- toYaml .Values.podLabels | nindent 8 }} + spec: + nodeSelector: + kubernetes.io/os: linux + {{- toYaml .Values.nodeSelector | nindent 8 }} + tolerations: + {{- toYaml .Values.tolerations | nindent 8 }} + affinity: + {{- toYaml .Values.affinity | nindent 8 }} + containers: + {{- if gt (len .Values.providerContainer) 0 }} + {{- toYaml .Values.providerContainer | nindent 6 }} + {{- end }} + - name: manager + image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - --provider-volume=/provider + - --token-request-audience={{ include "secret-sync-controller.listToString" . }} + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:{{ .Values.metricsPort }} + - --leader-elect + - --rotation-poll-interval={{ .Values.rotationPollInterval }} + env: + - name: SYNC_CONTROLLER_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: SYNC_CONTROLLER_POD_UID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: SYNC_CONTROLLER_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + ports: + - name: metrics + containerPort: {{ .Values.metricsPort }} + protocol: TCP + name: manager + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + command: + - /manager +{{- with .Values.resources }} + resources: +{{ toYaml . | indent 10 }} +{{- end }} + volumeMounts: + - mountPath: "/provider" + name: providervol + serviceAccountName: "secret-sync-controller-manager" + terminationGracePeriodSeconds: 10 + volumes: + - name: providervol + hostPath: + path: "/var/run/secrets-store-sync-providers" + type: DirectoryOrCreate diff --git a/secret-sync-controller/charts/secretsync/templates/secretsync_editor_role.yaml b/secret-sync-controller/charts/secretsync/templates/secretsync_editor_role.yaml new file mode 100644 index 000000000..ecb449b1c --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/secretsync_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit secretsyncs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: secretsync-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: secretsync-editor-role +rules: +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs/status + verbs: + - get diff --git a/secret-sync-controller/charts/secretsync/templates/secretsync_viewer_role.yaml b/secret-sync-controller/charts/secretsync/templates/secretsync_viewer_role.yaml new file mode 100644 index 000000000..869e72311 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/secretsync_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view secretsyncs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: secretsync-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: secretsync-viewer-role +rules: +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs + verbs: + - get + - list + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs/status + verbs: + - get diff --git a/secret-sync-controller/charts/secretsync/templates/update_owners_check_old_object.yaml b/secret-sync-controller/charts/secretsync/templates/update_owners_check_old_object.yaml new file mode 100644 index 000000000..3d093e4bb --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/update_owners_check_old_object.yaml @@ -0,0 +1,30 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-update-check-oldobject-policy" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: {{ include "chartname.matchConditionExpression" . | quote }} + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["secrets"] + variables: + - name: hasOneSecretSyncOwner + expression: "has(oldObject.metadata.ownerReferences) && (size(oldObject.metadata.ownerReferences) == 1 && oldObject.metadata.ownerReferences.all(o, o.kind == 'SecretSync' && o.apiVersion.startsWith('secret-sync.x-k8s.io/') && o.name == object.metadata.name))" + - name: allowedSecretTypes + expression: {{ include "chartname.secretTypesList" .Values.validatingAdmissionPolicies.allowedSecretTypes | quote }} + validations: + - expression: "variables.allowedSecretTypes == true && variables.hasOneSecretSyncOwner == true" + message: "Only secrets with one secret sync owner and with types defined in the config_allow_secret_types configmap are allowed" + messageExpression: "string(params.data.controllerName) + ' has failed to ' + string(request.operation) + ' old secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace. The controller can only update secrets in the allowed types list with a single secretsync owner.'" + reason: "Forbidden" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/update_secrets_based_on_label.yaml b/secret-sync-controller/charts/secretsync/templates/update_secrets_based_on_label.yaml new file mode 100644 index 000000000..6e8387ad0 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/update_secrets_based_on_label.yaml @@ -0,0 +1,28 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-update-label-policy" +spec: + failurePolicy: Fail + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: {{ include "chartname.matchConditionExpression" . | quote }} + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["secrets"] + variables: + - name: oldSecretHasLabels + expression: "has(oldObject.metadata.labels) ? true : false" + - name: oldSecretHasExpectedLabelKey + expression: {{ include "secret-sync-controller.oldSecretHasExpectedLabelKey" . | quote }} + - name: oldSecretHasExpectedLabelValue + expression: {{ include "secret-sync-controller.oldSecretHasExpectedLabelValue" . | quote }} + validations: + - expression: "variables.oldSecretHasExpectedLabelKey && variables.oldSecretHasExpectedLabelValue" + message: "Only secrets with the correct label can be updated" + messageExpression: "'secret-sync-controller has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace because it does not have the correct label. Delete the secret and force the controller to recreate it with the correct label.'" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/validate_annotation_format_secret_sync.yaml b/secret-sync-controller/charts/secretsync/templates/validate_annotation_format_secret_sync.yaml new file mode 100644 index 000000000..213c15c86 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/validate_annotation_format_secret_sync.yaml @@ -0,0 +1,26 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-validate-annotation-policy" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["secret-sync.x-k8s.io"] + apiVersions: ["*"] + operations: ["CREATE", "UPDATE"] + resources: ["secretsyncs"] + variables: + - name: secretHasAnnotation + expression: "has(object.spec.secretObject.annotations) ? true : false" + - name: secretHasCorrectAnnotationsFormat + expression: "variables.secretHasAnnotation && object.spec.secretObject.annotations.all(x, size(x) < 253 && x.matches('^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])?(/[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])*)?$') == true)" + - name: secretHasCorrectAnnotationsValueFormat + expression: "variables.secretHasAnnotation && object.spec.secretObject.annotations.all(x, size(object.spec.secretObject.annotations[x]) < 63 && object.spec.secretObject.annotations[x].matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$') == true)" + validations: + - expression: "variables.secretHasAnnotation == false || (variables.secretHasCorrectAnnotationsFormat && variables.secretHasCorrectAnnotationsValueFormat) == true" + message: "One of the annotations applied on the secret has an invalid format. Update the annotation and try again." + messageExpression: "string(request.userInfo.username) + ' has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace. One of the annotations applied on the secret has an invalid format. Update the annotation and try again.'" + reason: "Invalid" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/validate_label_format_secret_sync.yaml b/secret-sync-controller/charts/secretsync/templates/validate_label_format_secret_sync.yaml new file mode 100644 index 000000000..0f60a8eed --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/validate_label_format_secret_sync.yaml @@ -0,0 +1,26 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-validate-label-policy" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["secret-sync.x-k8s.io"] + apiVersions: ["*"] + operations: ["CREATE", "UPDATE"] + resources: ["secretsyncs"] + variables: + - name: secretHasLabel + expression: "has(object.spec.secretObject.labels) ? true : false" + - name: secretHasCorrectLabelsFormat + expression: "variables.secretHasLabel && object.spec.secretObject.labels.all(x, size(x) < 253 && x.matches('^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])?(/[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])*)?$') == true)" + - name: secretHasCorrectLabelsValueFormat + expression: "variables.secretHasLabel && object.spec.secretObject.labels.all(x, size(object.spec.secretObject.labels[x]) < 63 && object.spec.secretObject.labels[x].matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$') == true)" + validations: + - expression: "variables.secretHasLabel == false || (variables.secretHasCorrectLabelsFormat && variables.secretHasCorrectLabelsValueFormat) == true" + message: "One of the labels applied on the secret has an invalid format. Update the label and try again." + messageExpression: "string(request.userInfo.username) + ' has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace because it does not have the correct label. Delete the secret and force the controller to recreate it with the correct label.'" + reason: "Invalid" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/templates/validate_token_config.yaml b/secret-sync-controller/charts/secretsync/templates/validate_token_config.yaml new file mode 100644 index 000000000..0e86be657 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/templates/validate_token_config.yaml @@ -0,0 +1,29 @@ +{{- if .Values.validatingAdmissionPolicies.applyPolicies -}} +{{ include "secret-sync-controller.admissionApiVersion" . }} +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-validate-token-policy" +spec: + failurePolicy: Fail + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: {{ include "chartname.matchConditionExpression" . | quote }} + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["serviceaccounts/token"] + variables: + - name: expirationSeconds + expression: "string(object.spec.expirationSeconds) == '600'" + - name: hasCorrectAudience + expression: {{ include "secret-sync-controller.tokenAudienceComparison" . | quote }} + - name: requestHasOnlyOneAudience + expression: "object.spec.audiences.size() == 1" + validations: + - expression: "variables.hasCorrectAudience == true && variables.expirationSeconds == true && variables.requestHasOnlyOneAudience == true" + message: "'Creating a serviceaccount token has failed because the configuration isn't correct.'" + messageExpression: "'secret-sync-controller has failed to ' + string(request.operation) + ' ' + string(request.name) + ' token in the ' + string(request.namespace) + ' namespace. Check the configuration.'" + reason: "Forbidden" +{{- end -}} diff --git a/secret-sync-controller/charts/secretsync/values.yaml b/secret-sync-controller/charts/secretsync/values.yaml new file mode 100644 index 000000000..c5d1e3929 --- /dev/null +++ b/secret-sync-controller/charts/secretsync/values.yaml @@ -0,0 +1,92 @@ +# Default values for secretsync. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +controllerName: secret-sync-controller-manager +namespace: secret-sync-controller-system + +tokenRequestAudience: + - audience: # e.g. api://TokenAudienceExample + +rotationPollInterval: 21600s +logVerbosity: 5 + +validatingAdmissionPolicies: + applyPolicies: true + kubernetesReleaseVersion: "1.28.0" + allowedSecretTypes: + - "Opaque" + - "kubernetes.io/basic-auth" + - "bootstrap.kubernetes.io/token" + - "kubernetes.io/dockerconfigjson" + - "kubernetes.io/dockercfg" + - "kubernetes.io/ssh-auth" + - "kubernetes.io/tls" + + deniedSecretTypes: + - "kubernetes.io/service-account-token" + +image: + repository: aramase/secrets-sync-controller # e.g. my-registry.example.com/my-repo + pullPolicy: IfNotPresent + tag: "v0.0.1" + +securityContext: + # Default values, can be overridden or extended + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + +podAnnotations: + kubectl.kubernetes.io/default-container: manager + +podLabels: + control-plane: controller-manager + secrets-store.io/system: "true" + +tolerations: +- operator: Exists + +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: type + operator: NotIn + values: + - virtual-kubelet + +metricsPort: 8085 + +providerContainer: +# - name: provider-e2e-installer +# image: aramase/e2e-provider:v0.0.1 +# imagePullPolicy: IfNotPresent +# args: +# - --endpoint=unix:///provider/e2e-provider.sock +# resources: +# requests: +# cpu: 50m +# memory: 100Mi +# limits: +# cpu: 50m +# memory: 100Mi +# securityContext: +# allowPrivilegeEscalation: false +# readOnlyRootFilesystem: true +# runAsUser: 0 +# capabilities: +# drop: +# - ALL +# volumeMounts: +# - mountPath: "/provider" +# name: providervol diff --git a/secret-sync-controller/config/crd/bases/secret-sync.x-k8s.io_secretsyncs.yaml b/secret-sync-controller/config/crd/bases/secret-sync.x-k8s.io_secretsyncs.yaml new file mode 100644 index 000000000..0cf5b1639 --- /dev/null +++ b/secret-sync-controller/config/crd/bases/secret-sync.x-k8s.io_secretsyncs.yaml @@ -0,0 +1,268 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: secretsyncs.secret-sync.x-k8s.io +spec: + group: secret-sync.x-k8s.io + names: + kind: SecretSync + listKind: SecretSyncList + plural: secretsyncs + singular: secretsync + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: SecretSync represents the desired state and observed state of + the secret synchronization process. The SecretSync name is used to as the + secret object created by the controller. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretSyncSpec defines the desired state for synchronizing + secret. + properties: + forceSynchronization: + description: forceSynchronization can be used to force the secret + synchronization. The secret synchronization is triggered, by changing + the value in this field. This field is not used to resolve synchronization + conflicts. It is not related with the force query parameter in the + Apply operation. https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts + maxLength: 253 + pattern: ^[A-Za-z0-9]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)? + type: string + secretObject: + description: secretObject specifies the configuration for the synchronized + Kubernetes secret object. + properties: + annotations: + additionalProperties: + type: string + description: 'annotations contains key-value pairs representing + annotations associated with the Kubernetes secret object. The + following annotation prefix is reserved: secrets-store.sync.x-k8s.io/. + Creation fails if the annotation key is specified in the SecretSync + object by the user.' + type: object + x-kubernetes-validations: + - message: Annotations should have < 253 characters for both keys + and values. + rule: (self.all(x, x.size() < 253 && self[x].size() < 253) == + true) + - message: Annotations should not contain secrets-store.sync.x-k8s.io. + This key is reserved for the controller. + rule: (self.all(x, x.startsWith('secrets-store.sync.x-k8s.io') + == false)) + data: + description: data is a list of SecretObjectData containing secret + data source from the Secret Provider Class and the corresponding + data field key used in the Kubernetes secret object. + items: + description: SecretObjectData defines the desired state of synchronized + data within a Kubernetes secret object. + properties: + sourcePath: + description: sourcePath is the data source value of the + secret defined in the Secret Provider Class. This matches + the path of a file in the MountResponse returned from + the provider. + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9.]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)?(\/([0-9]+))*$ + type: string + targetKey: + description: 'targetKey is the key in the Kubernetes secret''s + data field as described in the Kubernetes API reference: + https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/' + maxLength: 253 + minLength: 1 + pattern: ^[A-Za-z0-9.]([-A-Za-z0-9]+([-._a-zA-Z0-9]?[A-Za-z0-9])*)?(\/([0-9]+))*$ + type: string + required: + - sourcePath + - targetKey + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - targetKey + x-kubernetes-list-type: map + labels: + additionalProperties: + type: string + description: 'labels contains key-value pairs representing labels + associated with the Kubernetes secret object. The labels are + used to identify the secret object created by the controller. + On secret creation, the following label is added: secrets-store.sync.x-k8s.io/secretsync=. + The following label prefix is reserved: secrets-store.sync.x-k8s.io/. + Creation fails if the label is specified in the SecretSync object + with a different value. On secret update, if the validation + admission policy is set, the controller will check if the label + secrets-store.sync.x-k8s.io/secretsync= is + present. If the label is not present, controller fails to update + the secret.' + type: object + x-kubernetes-validations: + - message: Labels should have < 63 characters for both keys and + values. + rule: (self.all(x, x.size() < 63 && self[x].size() < 63) == + true) + - message: Labels should not contain secrets-store.sync.x-k8s.io. + This key is reserved for the controller. + rule: (self.all(x, x.startsWith('secrets-store.sync.x-k8s.io') + == false)) + type: + description: type specifies the type of the Kubernetes secret + object, e.g. "Opaque";"kubernetes.io/basic-auth";"kubernetes.io/ssh-auth";"kubernetes.io/tls" + The controller must have permission to create secrets of the + specified type. + maxLength: 253 + minLength: 1 + type: string + required: + - data + - type + type: object + secretProviderClassName: + description: secretProviderClassName specifies the name of the secret + provider class used to pass information to access the secret store. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + secretSyncControllerName: + default: "" + description: secretSyncControllerName specifies the name of the secret + sync controller used to synchronize the secret. + type: string + serviceAccountName: + description: serviceAccountName specifies the name of the service + account used to access the secret store. The audience field in the + service account token must be passed as parameter in the controller + configuration. The audience is used when requesting a token from + the API server for the service account; the supported audiences + are defined by each provider. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - secretObject + - secretProviderClassName + - serviceAccountName + type: object + status: + description: SecretSyncStatus defines the observed state of the secret + synchronization process. + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastSuccessfulSyncTime: + description: lastSuccessfulSyncTime represents the last time the secret + was retrieved from the Provider and updated. + format: date-time + type: string + syncHash: + description: 'syncHash contains the hash of the secret object data, + data from the SecretProviderClass (e.g. UID, and metadata.generation), + and similar data from the SecretSync. This hash is used to determine + if the secret changed. The hash is calculated using the HMAC (Hash-based + Message Authentication Code) algorithm, using bcrypt hashing, with + the SecretsSync''s UID as the key. The secret is updated if: 1. + the hash is different 2. the lastSuccessfulSyncTime indicates a + rotation is required - the rotation poll interval is passed as a + parameter in the controller configuration 3. the SecretUpdateStatus + is ''Failed''' + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/secret-sync-controller/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/secret-sync-controller/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml new file mode 100644 index 000000000..fcf63c668 --- /dev/null +++ b/secret-sync-controller/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -0,0 +1,177 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.1 + name: secretproviderclasses.secrets-store.csi.x-k8s.io +spec: + group: secrets-store.csi.x-k8s.io + names: + kind: SecretProviderClass + listKind: SecretProviderClassList + plural: secretproviderclasses + singular: secretproviderclass + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: SecretProviderClass is the Schema for the secretproviderclasses + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretProviderClassSpec defines the desired state of SecretProviderClass + properties: + parameters: + additionalProperties: + type: string + description: Configuration for specific provider + type: object + provider: + description: Configuration for provider name + type: string + secretObjects: + items: + description: SecretObject defines the desired state of synced K8s + secret objects + properties: + annotations: + additionalProperties: + type: string + description: annotations of k8s secret object + type: object + data: + items: + description: SecretObjectData defines the desired state of + synced K8s secret object data + properties: + key: + description: data field to populate + type: string + objectName: + description: name of the object to sync + type: string + type: object + type: array + labels: + additionalProperties: + type: string + description: labels of K8s secret object + type: object + secretName: + description: name of the K8s secret object + type: string + type: + description: type of K8s secret object + type: string + type: object + type: array + type: object + status: + description: SecretProviderClassStatus defines the observed state of SecretProviderClass + type: object + type: object + served: true + storage: true + - deprecated: true + deprecationWarning: secrets-store.csi.x-k8s.io/v1alpha1 is deprecated. Use secrets-store.csi.x-k8s.io/v1 + instead. + name: v1alpha1 + schema: + openAPIV3Schema: + description: SecretProviderClass is the Schema for the secretproviderclasses + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SecretProviderClassSpec defines the desired state of SecretProviderClass + properties: + parameters: + additionalProperties: + type: string + description: Configuration for specific provider + type: object + provider: + description: Configuration for provider name + type: string + secretObjects: + items: + description: SecretObject defines the desired state of synced K8s + secret objects + properties: + annotations: + additionalProperties: + type: string + description: annotations of k8s secret object + type: object + data: + items: + description: SecretObjectData defines the desired state of + synced K8s secret object data + properties: + key: + description: data field to populate + type: string + objectName: + description: name of the object to sync + type: string + type: object + type: array + labels: + additionalProperties: + type: string + description: labels of K8s secret object + type: object + secretName: + description: name of the K8s secret object + type: string + type: + description: type of K8s secret object + type: string + type: object + type: array + type: object + status: + description: SecretProviderClassStatus defines the observed state of SecretProviderClass + properties: + byPod: + items: + description: ByPodStatus defines the state of SecretProviderClass + as seen by an individual controller + properties: + id: + description: id of the pod that wrote the status + type: string + namespace: + description: namespace of the pod that wrote the status + type: string + type: object + type: array + type: object + type: object + served: true + storage: false diff --git a/secret-sync-controller/config/crd/kustomization.yaml b/secret-sync-controller/config/crd/kustomization.yaml new file mode 100644 index 000000000..f27f08ec2 --- /dev/null +++ b/secret-sync-controller/config/crd/kustomization.yaml @@ -0,0 +1,22 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/secret-sync.x-k8s.io_secretsyncs.yaml +- bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_secretsyncs.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_secretsyncs.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +#- kustomizeconfig.yaml diff --git a/secret-sync-controller/config/default/kustomization.yaml b/secret-sync-controller/config/default/kustomization.yaml new file mode 100644 index 000000000..9cb0a29d2 --- /dev/null +++ b/secret-sync-controller/config/default/kustomization.yaml @@ -0,0 +1,76 @@ +# Adds namespace to all resources. +namespace: secret-sync-controller-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: secret-sync-controller- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager + +# ValidatingAdmissionWebhook are optional. If your K8S cluster version is lower than 1.28.0, you can disable it. +- ../validatingadmissionpolicies + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +# - manager_auth_proxy_patch.yaml + + + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/secret-sync-controller/config/default/manager_config_patch.yaml b/secret-sync-controller/config/default/manager_config_patch.yaml new file mode 100644 index 000000000..1ae6c5310 --- /dev/null +++ b/secret-sync-controller/config/default/manager_config_patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/secret-sync-controller/config/manager/kustomization.yaml b/secret-sync-controller/config/manager/kustomization.yaml new file mode 100644 index 000000000..e6b3ee1e9 --- /dev/null +++ b/secret-sync-controller/config/manager/kustomization.yaml @@ -0,0 +1,8 @@ +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: secret-sync-controller + newTag: v0.0.1 diff --git a/secret-sync-controller/config/manager/manager.yaml b/secret-sync-controller/config/manager/manager.yaml new file mode 100644 index 000000000..3bf9af651 --- /dev/null +++ b/secret-sync-controller/config/manager/manager.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: type + operator: NotIn + values: + - virtual-kubelet + tolerations: + - operator: Exists + nodeSelector: + kubernetes.io/os: linux + #securityContext: + # runAsNonRoot: true + containers: + - name: provider-e2e-installer + image: aramase/e2e-provider:v0.0.1 # replace this with your provider image, the e2e-provider image is available at aramase/e2e-provider:v0.0.1 + imagePullPolicy: IfNotPresent + args: + - --endpoint=unix:///provider/e2e-provider.sock + resources: + requests: + cpu: 50m + memory: 100Mi + limits: + cpu: 50m + memory: 100Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsUser: 0 + capabilities: + drop: + - ALL + volumeMounts: + - mountPath: "/provider" + name: providervol + - name: manager + image: secret-sync-controller:v0.0.1 # replace this with your acr image, the pre-alpha version of the controller is available at aramase/secrets-sync-controller:v0.0.1 + ports: + - name: metrics + containerPort: 8085 + protocol: TCP + imagePullPolicy: IfNotPresent + args: + - --provider-volume=/provider + #- --token-request-audience=token-audience # replace this with your token audience + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8085 + - --leader-elect + #- --rotation-poll-interval=60s + command: + - /manager + env: + - name: SYNC_CONTROLLER_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: SYNC_CONTROLLER_POD_UID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: SYNC_CONTROLLER_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + volumeMounts: + - mountPath: "/provider" + name: providervol + serviceAccountName: ssc-manager + terminationGracePeriodSeconds: 10 + volumes: + - name: providervol + hostPath: + path: "/var/run/secrets-store-sync-providers" + type: DirectoryOrCreate + diff --git a/secret-sync-controller/config/manifests/kustomization.yaml b/secret-sync-controller/config/manifests/kustomization.yaml new file mode 100644 index 000000000..71adb32eb --- /dev/null +++ b/secret-sync-controller/config/manifests/kustomization.yaml @@ -0,0 +1,26 @@ +# These resources constitute the fully configured set of manifests +# used to generate the 'manifests/' directory in a bundle. +resources: +- bases/secret-sync-controller.clusterserviceversion.yaml +- ../default +- ../samples + +# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. +# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. +# These patches remove the unnecessary "cert" volume and its manager container volumeMount. +#patchesJson6902: +#- target: +# group: apps +# version: v1 +# kind: Deployment +# name: controller-manager +# namespace: system +# patch: |- +# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. +# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. +# - op: remove +# path: /spec/template/spec/containers/1/volumeMounts/0 +# # Remove the "cert" volume, since OLM will create and mount a set of certs. +# # Update the indices in this path if adding or removing volumes in the manager's Deployment. +# - op: remove +# path: /spec/template/spec/volumes/0 diff --git a/secret-sync-controller/config/rbac/kustomization.yaml b/secret-sync-controller/config/rbac/kustomization.yaml new file mode 100644 index 000000000..e88276c83 --- /dev/null +++ b/secret-sync-controller/config/rbac/kustomization.yaml @@ -0,0 +1,12 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml + diff --git a/secret-sync-controller/config/rbac/leader_election_role.yaml b/secret-sync-controller/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000..c53accdd0 --- /dev/null +++ b/secret-sync-controller/config/rbac/leader_election_role.yaml @@ -0,0 +1,44 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/secret-sync-controller/config/rbac/leader_election_role_binding.yaml b/secret-sync-controller/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000..3bff01f83 --- /dev/null +++ b/secret-sync-controller/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: ssc-manager + namespace: system diff --git a/secret-sync-controller/config/rbac/role.yaml b/secret-sync-controller/config/rbac/role.yaml new file mode 100644 index 000000000..3b7cbdddd --- /dev/null +++ b/secret-sync-controller/config/rbac/role.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - patch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs + verbs: + - get + - list + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs/status + verbs: + - get + - patch + - update +- apiGroups: + - secrets-store.csi.x-k8s.io + resources: + - secretproviderclasses + verbs: + - get + - list + - watch diff --git a/secret-sync-controller/config/rbac/role_binding.yaml b/secret-sync-controller/config/rbac/role_binding.yaml new file mode 100644 index 000000000..304900387 --- /dev/null +++ b/secret-sync-controller/config/rbac/role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: ssc-manager + namespace: system diff --git a/secret-sync-controller/config/rbac/secretsync_editor_role.yaml b/secret-sync-controller/config/rbac/secretsync_editor_role.yaml new file mode 100644 index 000000000..ecb449b1c --- /dev/null +++ b/secret-sync-controller/config/rbac/secretsync_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit secretsyncs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: secretsync-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: secretsync-editor-role +rules: +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs/status + verbs: + - get diff --git a/secret-sync-controller/config/rbac/secretsync_viewer_role.yaml b/secret-sync-controller/config/rbac/secretsync_viewer_role.yaml new file mode 100644 index 000000000..869e72311 --- /dev/null +++ b/secret-sync-controller/config/rbac/secretsync_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view secretsyncs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: secretsync-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: secretsync-viewer-role +rules: +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs + verbs: + - get + - list + - watch +- apiGroups: + - secret-sync.x-k8s.io + resources: + - secretsyncs/status + verbs: + - get diff --git a/secret-sync-controller/config/rbac/service_account.yaml b/secret-sync-controller/config/rbac/service_account.yaml new file mode 100644 index 000000000..31c0e72bc --- /dev/null +++ b/secret-sync-controller/config/rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: secret-sync-controller + app.kubernetes.io/part-of: secret-sync-controller + app.kubernetes.io/managed-by: kustomize + name: ssc-manager + namespace: system diff --git a/secret-sync-controller/config/validatingadmissionpolicies/config_allow_secret_types.yaml b/secret-sync-controller/config/validatingadmissionpolicies/config_allow_secret_types.yaml new file mode 100644 index 000000000..3232b14f9 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/config_allow_secret_types.yaml @@ -0,0 +1,22 @@ +# This is the config map for the allow policy. These values will be passed to the helm chart. +# The config map is an easy way to provide an example of the configuration. +# It is used to configure the controllerName, controllerNamespace parameters and the list of secret types to allow. +# The user must configure: +# the controllerName and controllerNamespace parameters +# the list of secret types to allow +# These pre-configured types are currently supported by the Secret Store CSI Driver. +# We don't want to allow the creation of any other types of secrets: e.g. service-account-tokens +apiVersion: v1 +kind: ConfigMap +metadata: + name: "admission-policies-secret-sync-controller-allow-config" +data: + controllerName: 'secret-sync-controller' # This is the name of the controller. It is formatted in the policy as: system:serviceaccount:NS_NAME:secret-store-sync-controller + controllerNamespace: 'secret-sync-controller' # This is the namespace where the secret-sync-controller is running + secretTypeOpaque: 'Opaque' + secretTypeBasicAuth: 'kubernetes.io/basic-auth' + secretTypeBootstrapToken: 'bootstrap.kubernetes.io/token' + secretTypeDockerConfigJson: 'kubernetes.io/dockerconfigjson' + secretTypeDockerCfg: 'kubernetes.io/dockercfg' + secretTypeSSHAuth: 'kubernetes.io/ssh-auth' + secretTypeTLS: 'kubernetes.io/tls' \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/config_deny_secret_types.yaml b/secret-sync-controller/config/validatingadmissionpolicies/config_deny_secret_types.yaml new file mode 100644 index 000000000..8abf20b63 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/config_deny_secret_types.yaml @@ -0,0 +1,15 @@ +# This is the config map for the deny policy. These values will be passed to the helm chart. +# The config map is an easy way to provide an example of the configuration. +# It is used to configure the controllerName, controllerNamespace parameters and the list of secret types to denny. +# The user must configure: +# the controllerName and controllerNamespace parameters +# the list of secret types to denny +apiVersion: v1 +kind: ConfigMap +metadata: + name: "admission-policies-secret-sync-controller-deny-config" +data: + controllerName: 'secret-sync-controller' + controllerNamespace: 'secret-sync-controller' + secretTypeServiceAccountToken: 'kubernetes.io/service-account-token' + \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/config_ssc.yaml b/secret-sync-controller/config/validatingadmissionpolicies/config_ssc.yaml new file mode 100644 index 000000000..aa0a3d74f --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/config_ssc.yaml @@ -0,0 +1,19 @@ +# This is the configuration file for the Secret Sync Controller. These values will be passed to the helm chart. +# The config map is an easy way to provide an example of the configuration. +# The user must configure: +# the controllerName and controllerNamespace parameters +# the list of Audiences: they should have the format tokenAudience: 'audience' +# The user should NOT configure: +# the labelKey and labelValue parameters +# the maxExpirationSeconds parameter +apiVersion: v1 +kind: ConfigMap +metadata: + name: "admission-policies-secret-sync-controller-config" +data: + labelKey: 'secrets-sync-controller.k8s.io/managed' + labelValue: '' + controllerName: 'secret-sync-controller' + controllerServiceAccountName: 'secret-sync-controller-sa' + controllerNamespace: 'secret-sync-controller' + tokenAudience1: 'api://TokenAudienceExample' \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/create_update_secrets_types.yaml b/secret-sync-controller/config/validatingadmissionpolicies/create_update_secrets_types.yaml new file mode 100644 index 000000000..530c7a511 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/create_update_secrets_types.yaml @@ -0,0 +1,34 @@ +# This policy is used to restrict the types of secrets that the controller can create or update. +# It requires the parameter controllerName, controllerNamespace and the list of secret types to allow to be set in the +# ConfigMap admission-policies-secret-sync-controller-config. +# The user must configure: +# the controllerName and controllerNamespace parameters +# the list of secret types to allow +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-create-update-policy" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: "request.userInfo.username == 'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName" #'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["secrets"] + variables: + - name: hasOneSecretSyncOwner + expression: "has(object.metadata.ownerReferences) && (size(object.metadata.ownerReferences) == 1 && object.metadata.ownerReferences.all(o, o.kind == 'SecretSync' && o.apiVersion.startsWith('secret-sync.x-k8s.io/') && o.name == object.metadata.name))" + - name: allowedSecretTypes + expression: "params.data.exists_one(x, params.data[x] == object.type) ? true : false" + validations: + - expression: "variables.allowedSecretTypes == true && variables.hasOneSecretSyncOwner == true" + message: "Only secrets with types defined in the admission-policies-secret-sync-controller-allow-config configmap are allowed" + messageExpression: "string(params.data.controllerName) + ' has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace. The controller can only create or update secrets in the allowed types list with a single secretsync owner.'" + reason: "Forbidden" \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/delete_secrets.yaml b/secret-sync-controller/config/validatingadmissionpolicies/delete_secrets.yaml new file mode 100644 index 000000000..432919f16 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/delete_secrets.yaml @@ -0,0 +1,28 @@ +# This policy is used to prevent the secret-sync-controller from deleting secrets. +# It requires the parameter controllerName and controllerNamespace to be set in the +# ConfigMap admission-policies-secret-sync-controller-config. +# The user must configure: +# the controllerName and controllerNamespace parameters +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-delete-policy" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConditions: + - name: 'userIsController' + expression: "request.userInfo.username == 'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName" + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["DELETE"] + resources: ["secrets"] + validations: + - expression: "false" # if the expression evaluates to false, the validation check is enforced according to the failurePolicy + message: "The controller is not allowed to delete secrets." + messageExpression: string(params.data.controllerName) + ' has failed to ' + string(request.operation) + ' secrets in the ' + string(request.namespace) + ' namespace. The controller is not allowed to delete secrets.' + reason: "Forbidden" \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/kustomization.yaml b/secret-sync-controller/config/validatingadmissionpolicies/kustomization.yaml new file mode 100644 index 000000000..2945fdba7 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/kustomization.yaml @@ -0,0 +1,15 @@ +resources: +# All ValidatingAdmissionPolicies will be applied under this service account in +# the deployment namespace. Be sure to change the config maps if you want to +# change the policies. +# Comment the policies you want to disable. +- config_ssc.yaml +- config_allow_secret_types.yaml +- config_deny_secret_types.yaml +- create_update_secrets_types.yaml +- delete_secrets.yaml +- update_owners_check_old_object.yaml +- update_secrets_based_on_label.yaml +- validate_annotation_format.yaml +- validate_label_format.yaml +- validate_token_config.yaml diff --git a/secret-sync-controller/config/validatingadmissionpolicies/update_owners_check_old_object.yaml b/secret-sync-controller/config/validatingadmissionpolicies/update_owners_check_old_object.yaml new file mode 100644 index 000000000..8a8c0a938 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/update_owners_check_old_object.yaml @@ -0,0 +1,50 @@ +# Copyright 2024 The Kubernetes Authors. +# +# 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. + +# This policy is used to restrict the types of secrets that the controller can update. The object which gets updated +# must have a single owner reference of type SecretSync. Since only the UPDATE operation has an oldObject, the policy +# can't be merged into the "CREATE" policy. +# It requires the parameter controllerName, controllerNamespace and the list of secret types to allow to be set in the +# ConfigMap admission-policies-secret-sync-controller-config. +# The user must configure: +# the controllerName and controllerNamespace parameters +# the list of secret types to allow +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-update-owners-check-oldobject-policy" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConditions: + - name: 'user-is-secret-sync-controller' + expression: "request.userInfo.username == 'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName" #'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["secrets"] + variables: + - name: hasOneSecretSyncOwner + expression: "has(oldObject.metadata.ownerReferences) && (size(oldObject.metadata.ownerReferences) == 1 && oldObject.metadata.ownerReferences.all(o, o.kind == 'SecretSync' && o.apiVersion.startsWith('secret-sync.x-k8s.io/') && o.name == object.metadata.name))" + - name: allowedSecretTypes + expression: "params.data.exists_one(x, params.data[x] == oldObject.type) ? true : false" + validations: + - expression: "variables.allowedSecretTypes == true && variables.hasOneSecretSyncOwner == true" + message: "Only secrets with one secret sync owner and with types defined in the config_allow_secret_types configmap are allowed" + messageExpression: "string(params.data.controllerName) + ' has failed to ' + string(request.operation) + ' old secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace. The controller can only update secrets in the allowed types list with a single secretsync owner.'" + reason: "Forbidden" \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/update_secrets_based_on_label.yaml b/secret-sync-controller/config/validatingadmissionpolicies/update_secrets_based_on_label.yaml new file mode 100644 index 000000000..8cea6d94a --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/update_secrets_based_on_label.yaml @@ -0,0 +1,37 @@ +# This policy will deny updates to secrets that do not have the correct label. +# It requires the parameter controllerName and controllerNamespace to be set in the +# ConfigMap admission-policies-secret-sync-controller-config. +# The label key and value are set in the ConfigMap admission-policies-secret-sync-controller-allow-config. +# The label key and value shouldn't be changed by the user. +# The user must configure: +# the controllerName and controllerNamespace parameters +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-update-label-policy" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConditions: + - name: 'userIsController' + expression: "request.userInfo.username == 'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName" + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["secrets"] + variables: + - name: oldSecretHasLabels + expression: "has(oldObject.metadata.labels) ? true : false" + - name: oldSecretHasExpectedLabelKey + expression: "variables.oldSecretHasLabels && ((params.data.labelKey in oldObject.metadata.labels) ? true : false)" + - name: oldSecretHasExpectedLabelValue + expression: "params.data.labelValue != null ? (variables.oldSecretHasExpectedLabelKey && ((params.data.labelValue == oldObject.metadata.labels[params.data.labelKey]) ? true : false)) : (oldObject.metadata.labels[params.data.labelKey] ? false : true)" + validations: + - expression: "variables.oldSecretHasExpectedLabelKey && variables.oldSecretHasExpectedLabelValue" # if the expression evaluates to false, the validation check is enforced according to the failurePolicy + message: "Only secrets with the correct label can be updated" + messageExpression: "string(params.data.controllerName) + ' has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace because it does not have the correct label. Delete the secret and force the controller to recreate it with the correct label.'" + reason: "Invalid" \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/validate_annotation_format.yaml b/secret-sync-controller/config/validatingadmissionpolicies/validate_annotation_format.yaml new file mode 100644 index 000000000..ef7a89824 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/validate_annotation_format.yaml @@ -0,0 +1,24 @@ +# This policy checks if the secretObject.annotations field has the correct format. +# The annotation key should be a valid DNS subdomain name. +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-validate-annotation-policy" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["secrets-store.sync.x-k8s.io"] + apiVersions: ["*"] + operations: ["CREATE", "UPDATE"] + resources: ["secretsyncs"] + variables: + - name: secretHasAnnotation + expression: "has(object.spec.secretObject.annotations) ? true : false" + - name: secretHasCorrectAnnotationsFormat + expression: "variables.secretHasAnnotation && object.spec.secretObject.annotations.all(x, size(x) < 253 && x.matches('^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])?(/[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])*)?$') == true)" + validations: + - expression: "variables.secretHasAnnotation == false || variables.secretHasCorrectAnnotationsFormat" + message: "One of the annotations applied on the secret has an invalid format. Update the annotation and try again." + messageExpression: "string(request.userInfo.username) + ' has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace because it does not have the correct annotation. Delete the secret and force the controller to recreate it with the correct annotation.'" + reason: "Invalid" \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/validate_label_format.yaml b/secret-sync-controller/config/validatingadmissionpolicies/validate_label_format.yaml new file mode 100644 index 000000000..ec8d21fc0 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/validate_label_format.yaml @@ -0,0 +1,26 @@ +# This policy checks if the secretObject.labels field has the correct format. +# The label key and value should be valid DNS subdomain names. +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-validate-label-policy" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["secrets-store.sync.x-k8s.io"] + apiVersions: ["*"] + operations: ["CREATE", "UPDATE"] + resources: ["secretsyncs"] + variables: + - name: secretHasLabel + expression: "has(object.spec.secretObject.labels) ? true : false" + - name: secretHasCorrectLabelsFormat + expression: "variables.secretHasLabel && object.spec.secretObject.labels.all(x, size(x) < 253 && x.matches('^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9])?(/[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])*)?$') == true)" + - name: secretHasCorrectLabelsValueFormat + expression: "variables.secretHasLabel && object.spec.secretObject.labels.all(x, size(object.spec.secretObject.labels[x]) < 63 && object.spec.secretObject.labels[x].matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$') == true)" + validations: + - expression: "variables.secretHasLabel == false || (variables.secretHasCorrectLabelsFormat && variables.secretHasCorrectLabelsValueFormat) == true" + message: "One of the labels applied on the secret has an invalid format. Update the label and try again." + messageExpression: "string(request.userInfo.username) + ' has failed to ' + string(request.operation) + ' secret with ' + string(object.type) + ' type ' + 'in the ' + string(request.namespace) + ' namespace because it does not have the correct label. Delete the secret and force the controller to recreate it with the correct label.'" + reason: "Invalid" \ No newline at end of file diff --git a/secret-sync-controller/config/validatingadmissionpolicies/validate_token_config.yaml b/secret-sync-controller/config/validatingadmissionpolicies/validate_token_config.yaml new file mode 100644 index 000000000..25c6f1071 --- /dev/null +++ b/secret-sync-controller/config/validatingadmissionpolicies/validate_token_config.yaml @@ -0,0 +1,36 @@ +# This policy validates the configuration of the token that is created by the secret-sync-controller. +# It requires the parameters controllerName, controllerNamespace, token expiration, and token audiences +# to be set in the ConfigMap admission-policies-secret-sync-controller-config. +# The user must configure: +# the token audiences +# the controllerName and controllerNamespace parameters +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "secret-sync-controller-validate-token-policy" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConditions: + - name: 'userIsController' + expression: "request.userInfo.username == 'system:serviceaccount:'+params.data.controllerNamespace+':'+params.data.controllerName" + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["serviceaccounts/token"] + variables: + - name: expirationSeconds + expression: "string(object.spec.expirationSeconds) == '600'" + - name: requestHasOnlyOneAudience + expression: "object.spec.audiences.size() == 1" + - name: hasCorrectAudience + expression: "params.data.exists_one(x, x.startsWith('tokenAudience') && object.spec.audiences.exists(w, w == params.data[x]))" # check any audience exists + validations: + - expression: "variables.hasCorrectAudience == true && variables.expirationSeconds == true && variables.requestHasOnlyOneAudience == true" # if the expression evaluates to false, the validation check is enforced according to the failurePolicy + message: "'Creating a serviceaccount token has failed because the configuration isn't correct.'" + messageExpression: "string(params.data.controllerName) + ' has failed to ' + string(request.operation) + ' ' + string(request.name) + ' token in the ' + string(request.namespace) + ' namespace. Check the configuration.'" + reason: "Forbidden" \ No newline at end of file diff --git a/secret-sync-controller/controllers/conditions.go b/secret-sync-controller/controllers/conditions.go new file mode 100644 index 000000000..9817d47ac --- /dev/null +++ b/secret-sync-controller/controllers/conditions.go @@ -0,0 +1,177 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 controllers + +import ( + "context" + + meta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/log" + secretsyncv1alpha1 "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/api/v1alpha1" +) + +const ( + ConditionReasonUnknown = "Unknown" + ConditionMessageUnknown = "Unknown" + ConditionTypeUnknown = "Unknown" + ConditionTypeCreate = "Create" + ConditionTypeUpdate = "Update" + + ConditionReasonCreateSucceeded = "CreateSucceeded" + ConditionMessageCreateSucceeded = "Secret created successfully." + + ConditionReasonFailedProviderError = "ProviderError" + ConditionMessageFailedProviderError = "Secret creation failed due to provider error, check the logs or the events for more information." + + ConditionReasonFailedInvalidLabelError = "InvalidClusterSecretLabelError" + ConditionMessageFailedInvalidLabelError = "The secret operation failed because a label reserved for the controller is applied on the secret." + + ConditionReasonFailedInvalidAnnotationError = "InvalidClusterSecretAnnotationError" + ConditionMessageFailedInvalidAnnotationError = "The secret create failed because an annotation reserved for the controller is applied on the secret." + + ConditionReasonUpdateNoValueChangeSucceeded = "UpdateNoValueChangeSucceeded" + ConditionMessageUpdateNoValueChangeSucceeded = "The secret was updated successfully at the end of the poll interval and no value change was detected." + + ConditionReasonUpdateValueChangeOrForceUpdateSucceeded = "UpdateValueChangeOrForceUpdateSucceeded" + ConditionMessageUpdateValueChangeOrForceUpdateSucceeded = "The secret was updated successfully: a value change or a force update was detected." + + ConditionReasonSecretPatchFailedUnknownError = "UnknownError" + ConditionMessageSecretPatchFailedUnknownError = "Secret patch failed due to unknown error, check the logs or the events for more information." + + ConditionReasonValidatingAdmissionPolicyCheckFailed = "ValidatingAdmissionPolicyCheckFailed" + ConditionMessageValidatingAdmissionPolicyCheckFailed = "Secret update failed due to validating admission policy check failure, check the logs or the events for more information." + + ConditionReasonControllerInternalError = "ControllerInternalError" + ConditionMessageControllerInternalError = "Secret update failed due to controller internal error, check the logs or the events for more information." + + ConditionReasonControllerSpcError = "ControllerSPCError" + ConditionMessageControllerSpcError = "Secret update failed because the controller could not retrieve the Secret Provider Class or the SPC is misconfigured. Check the logs or the events for more information." + + ConditionReasonUserInputValidationFailed = "UserInputValidationFailed" + ConditionMessageUserInputValidationFailed = "Secret create or update failed due to SecretProviderClass or SecretSync error, check the logs or the events for more information." +) + +var FailedConditionsTriggeringRetry = []string{ + ConditionReasonControllerSpcError, + ConditionReasonFailedInvalidAnnotationError, + ConditionReasonFailedInvalidLabelError, + ConditionReasonFailedProviderError, + ConditionReasonFailedInvalidAnnotationError, + ConditionReasonFailedProviderError, + ConditionReasonSecretPatchFailedUnknownError, + ConditionReasonValidatingAdmissionPolicyCheckFailed, + ConditionReasonUserInputValidationFailed, + ConditionTypeUnknown} + +var SucceededConditionsTriggeringRetry = []string{ + ConditionReasonCreateSucceeded, + ConditionReasonUpdateNoValueChangeSucceeded, + ConditionReasonUpdateValueChangeOrForceUpdateSucceeded} + +var AllowedStringsToDisplayConditionErrorMessage = []string{ + "validatingadmissionpolicy", +} + +func (r *SecretSyncReconciler) updateStatusConditions(ctx context.Context, ss *secretsyncv1alpha1.SecretSync, oldConditionType string, newConditionType string, conditionReason string, shouldUpdateStatus bool) { + logger := log.FromContext(ctx) + + if ss.Status.Conditions == nil { + ss.Status.Conditions = []metav1.Condition{} + } + + if len(oldConditionType) > 0 { + logger.V(10).Info("Removing old condition", "oldConditionType", oldConditionType) + meta.RemoveStatusCondition(&ss.Status.Conditions, oldConditionType) + } + + var condition metav1.Condition + switch conditionReason { + case ConditionReasonCreateSucceeded: + condition.Status = metav1.ConditionTrue + condition.Type = newConditionType + condition.Reason = ConditionReasonCreateSucceeded + condition.Message = ConditionMessageCreateSucceeded + case ConditionReasonUpdateNoValueChangeSucceeded: + condition.Status = metav1.ConditionTrue + condition.Type = newConditionType + condition.Reason = ConditionReasonUpdateNoValueChangeSucceeded + condition.Message = ConditionMessageUpdateNoValueChangeSucceeded + case ConditionReasonUpdateValueChangeOrForceUpdateSucceeded: + condition.Status = metav1.ConditionTrue + condition.Type = newConditionType + condition.Reason = ConditionReasonUpdateValueChangeOrForceUpdateSucceeded + condition.Message = ConditionMessageUpdateValueChangeOrForceUpdateSucceeded + case ConditionReasonValidatingAdmissionPolicyCheckFailed: + condition.Status = metav1.ConditionFalse + condition.Type = newConditionType + condition.Reason = ConditionReasonValidatingAdmissionPolicyCheckFailed + condition.Message = ConditionMessageValidatingAdmissionPolicyCheckFailed + case ConditionReasonFailedInvalidAnnotationError: + condition.Status = metav1.ConditionFalse + condition.Type = newConditionType + condition.Reason = ConditionReasonFailedInvalidAnnotationError + condition.Message = ConditionMessageFailedInvalidAnnotationError + case ConditionReasonFailedProviderError: + condition.Status = metav1.ConditionFalse + condition.Type = newConditionType + condition.Reason = ConditionReasonFailedProviderError + condition.Message = ConditionMessageFailedProviderError + case ConditionReasonFailedInvalidLabelError: + condition.Status = metav1.ConditionFalse + condition.Type = newConditionType + condition.Reason = ConditionReasonFailedInvalidLabelError + condition.Message = ConditionMessageFailedInvalidLabelError + case ConditionReasonSecretPatchFailedUnknownError: + condition.Status = metav1.ConditionUnknown + condition.Type = newConditionType + condition.Reason = ConditionReasonSecretPatchFailedUnknownError + condition.Message = ConditionMessageSecretPatchFailedUnknownError + case ConditionReasonUserInputValidationFailed: + condition.Status = metav1.ConditionFalse + condition.Type = newConditionType + condition.Reason = ConditionReasonUserInputValidationFailed + condition.Message = ConditionMessageUserInputValidationFailed + case ConditionReasonControllerSpcError: + condition.Status = metav1.ConditionFalse + condition.Type = newConditionType + condition.Reason = ConditionReasonControllerSpcError + condition.Message = ConditionMessageControllerSpcError + case ConditionReasonControllerInternalError: + condition.Status = metav1.ConditionUnknown + condition.Type = newConditionType + condition.Reason = ConditionReasonControllerInternalError + condition.Message = ConditionMessageControllerInternalError + default: + condition.Status = metav1.ConditionUnknown + condition.Type = ConditionTypeUnknown + condition.Reason = ConditionReasonUnknown + condition.Message = ConditionMessageUnknown + } + + logger.V(10).Info("Adding new condition", "newConditionType", newConditionType, "conditionReason", conditionReason) + meta.SetStatusCondition(&ss.Status.Conditions, condition) + + if !shouldUpdateStatus { + return + } + + if err := r.Client.Status().Update(ctx, ss); err != nil { + logger.Error(err, "Failed to update status", "condition", condition) + } + + logger.V(10).Info("Updated status", "condition", condition) +} diff --git a/secret-sync-controller/controllers/secretsync_controller.go b/secret-sync-controller/controllers/secretsync_controller.go new file mode 100644 index 000000000..0aa036db8 --- /dev/null +++ b/secret-sync-controller/controllers/secretsync_controller.go @@ -0,0 +1,467 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 controllers + +import ( + "context" + "crypto/hmac" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "slices" + "strings" + "time" + + "golang.org/x/crypto/pbkdf2" + corev1 "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + secretsstorecsiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" + secretsyncv1alpha1 "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/api/v1alpha1" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/pkg/k8s" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/pkg/provider" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/pkg/util/secretutil" +) + +const ( + // CSIPodName is the name of the pod that the mount is created for + CSIPodName = "csi.storage.k8s.io/pod.name" + + // CSIPodNamespace is the namespace of the pod that the mount is created for + CSIPodNamespace = "csi.storage.k8s.io/pod.namespace" + + // CSIPodUID is the UID of the pod that the mount is created for + CSIPodUID = "csi.storage.k8s.io/pod.uid" + + // CSIPodServiceAccountName is the name of the pod service account that the mount is created for + CSIPodServiceAccountName = "csi.storage.k8s.io/serviceAccount.name" + + // CSIPodServiceAccountTokens is the service account tokens of the pod that the mount is created for + CSIPodServiceAccountTokens = "csi.storage.k8s.io/serviceAccount.tokens" //nolint + + // Label applied by the controller to the secret object + ControllerLabelKey = "secrets-store.sync.x-k8s.io" + + // Label applied by the controller to the secret object + ControllerAnnotationKey = "secrets-store.sync.x-k8s.io" + + // Version is the version of the secret sync controller + Version = "v1" + + // SecretSyncControllerFieldManager is the field manager used by the secret sync controller + SecretSyncControllerFieldManager = Version + "-secret-sync-controller" + + // Environment variables set using downward API to pass as params to the controller + // Used to maintain the same logic as the Secrets Store CSI driver + SyncControllerPodName = "SYNC_CONTROLLER_POD_NAME" + SyncControllerPodUID = "SYNC_CONTROLLER_POD_UID" +) + +type AllClientBuilder interface { + Get(ctx context.Context, provider string) (v1alpha1.CSIDriverProviderClient, error) +} + +// SecretSyncReconciler reconciles a SecretSync object +type SecretSyncReconciler struct { + client.Client + Audiences []string + Clientset *kubernetes.Clientset + Scheme *runtime.Scheme + TokenClient *k8s.TokenClient + ProviderClients AllClientBuilder + RotationPollInterval time.Duration + EventRecorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=secret-sync.x-k8s.io,resources=secretsyncs,verbs=get;list;watch +//+kubebuilder:rbac:groups=secret-sync.x-k8s.io,resources=secretsyncs/status,verbs=get;update;patch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=create;patch;delete +//+kubebuilder:rbac:groups="",resources="serviceaccounts/token",verbs=create +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch +//+kubebuilder:rbac:groups=secrets-store.csi.x-k8s.io,resources=secretproviderclasses,verbs=get;list;watch + +func (r *SecretSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + + logger := log.FromContext(ctx) + logger.Info("Reconciling SecretSync", "namespace=", req.NamespacedName.String()) + + // get the secret sync object + ss := &secretsyncv1alpha1.SecretSync{} + if err := r.Get(ctx, req.NamespacedName, ss); err != nil { + logger.Error(err, "unable to fetch SecretSync") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // update status conditions: set them to unknown before processing as explained in the Kubernetes API conventions + // "Controllers should apply their conditions to a resource the first time they visit the resource, even if the status is Unknown." + r.updateStatusConditions(ctx, ss, "", ConditionTypeUnknown, ConditionReasonUnknown, true) + + // if the secret sync hash is empty, it means the secret does not exist, so the condition type is create + // otherwise, the condition type is update + conditionType := ConditionTypeUpdate + if len(ss.Status.SyncHash) == 0 { + conditionType = ConditionTypeCreate + } + + secretName := strings.TrimSpace(ss.Name) + + secretObj := ss.Spec.SecretObject + if err := secretutil.ValidateSecretObject(secretName, secretObj); err != nil { + logger.Error(err, "failed to validate secret object", "secretName", secretName) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonUserInputValidationFailed, true) + return ctrl.Result{}, err + } + + labels := make(map[string]string) + for k, v := range secretObj.Labels { + labels[k] = v + } + + if val, ok := labels[ControllerLabelKey]; ok && len(val) > 0 { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonFailedInvalidLabelError, true) + return ctrl.Result{}, fmt.Errorf("label %s is reserved for use by the secret sync controller", ControllerLabelKey) + } + labels[ControllerLabelKey] = "" + + annotations := make(map[string]string) + for k, v := range secretObj.Annotations { + annotations[k] = v + } + + if val, ok := annotations[ControllerAnnotationKey]; ok && len(val) > 0 { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonFailedInvalidAnnotationError, true) + return ctrl.Result{}, fmt.Errorf("annotation %s is reserved for use by the secret sync controller", ControllerAnnotationKey) + } + annotations[ControllerAnnotationKey] = "" + + // get the service account token + serviceAccountTokenAttrs, err := r.TokenClient.SecretProviderServiceAccountTokenAttrs(ss.Namespace, ss.Spec.ServiceAccountName, r.Audiences) + if err != nil { + logger.Error(err, "failed to get service account token", "name", ss.Spec.ServiceAccountName) + + if checkIfErrorMessageCanBeDisplayed(err.Error()) { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonValidatingAdmissionPolicyCheckFailed, true) + } else { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonSecretPatchFailedUnknownError, true) + } + return ctrl.Result{}, err + } + + // get the secret provider class object + spc := &secretsstorecsiv1.SecretProviderClass{} + if err := r.Get(ctx, client.ObjectKey{Name: ss.Spec.SecretProviderClassName, Namespace: req.Namespace}, spc); err != nil { + logger.Error(err, "failed to get secret provider class", "name", ss.Spec.SecretProviderClassName) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonControllerSpcError, true) + return ctrl.Result{}, err + } + + // this is to mimic the parameters sent from CSI driver to the provider + parameters := make(map[string]string) + for k, v := range spc.Spec.Parameters { + parameters[k] = v + } + + parameters[CSIPodName] = os.Getenv(SyncControllerPodName) + parameters[CSIPodUID] = os.Getenv(SyncControllerPodUID) + parameters[CSIPodNamespace] = req.Namespace + parameters[CSIPodServiceAccountName] = ss.Spec.ServiceAccountName + + for k, v := range serviceAccountTokenAttrs { + parameters[k] = v + } + + paramsJSON, err := json.Marshal(parameters) + if err != nil { + logger.Error(err, "failed to marshal parameters", "parameters", parameters) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonControllerInternalError, true) + return ctrl.Result{}, err + } + + providerName := string(spc.Spec.Provider) + providerClient, err := r.ProviderClients.Get(ctx, providerName) + if err != nil { + logger.Error(err, "failed to get provider client", "provider", providerName) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonControllerSpcError, true) + return ctrl.Result{}, err + } + + secretRefData := make(map[string]string) + var secretsJSON []byte + secretsJSON, err = json.Marshal(secretRefData) + if err != nil { + logger.Error(err, "failed to marshal secret") + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonControllerInternalError, true) + return ctrl.Result{}, err + } + + oldObjectVersions := make(map[string]string) + _, files, err := provider.MountContent(ctx, providerClient, string(paramsJSON), string(secretsJSON), oldObjectVersions) + if err != nil { + logger.Error(err, "failed to get secrets from provider", "provider", providerName) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonFailedProviderError, true) + return ctrl.Result{}, err + } + + secretType := secretutil.GetSecretType(strings.TrimSpace(secretObj.Type)) + var datamap map[string][]byte + if datamap, err = secretutil.GetSecretData(secretObj.Data, secretType, files); err != nil { + logger.Error(err, "failed to get secret data", "secretName", secretName) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonUserInputValidationFailed, true) + return ctrl.Result{}, err + } + + // Compute the hash of the secret + syncHash, err := r.computeSecretDataObjectHash(datamap, spc, ss) + if err != nil { + logger.Error(err, "failed to compute secret data object hash", "secretName", secretName) + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonControllerInternalError, true) + return ctrl.Result{}, err + } + + // Check if the hash has changed. + hashChanged := syncHash != ss.Status.SyncHash + + // Check if a secret create or update failed and if the controller should re-try the operation + failedCondition := metav1.Condition{} + for _, ssCondition := range ss.Status.Conditions { + if slices.Contains(FailedConditionsTriggeringRetry, ssCondition.Reason) { + failedCondition = ssCondition + break + } + } + + if len(failedCondition.Type) == 0 && !hashChanged { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonUpdateNoValueChangeSucceeded, true) + return ctrl.Result{}, nil + } + + if conditionType == ConditionTypeCreate { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonCreateSucceeded, false) + } else if hashChanged { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonUpdateValueChangeOrForceUpdateSucceeded, false) + } + + // Save current state for potential rollback. + prevSecretHash := ss.Status.SyncHash + prevTime := ss.Status.LastSuccessfulSyncTime + + // Update status fields. + ss.Status.LastSuccessfulSyncTime = &metav1.Time{Time: time.Now()} + ss.Status.SyncHash = syncHash + + if len(failedCondition.Type) != 0 { + meta.RemoveStatusCondition(&ss.Status.Conditions, failedCondition.Type) + } + + // Attempt to create or update the secret. + if err = r.serverSidePatchSecret(ctx, ss, secretName, req.Namespace, datamap, labels, annotations, secretType); err != nil { + logger.Error(err, "failed to patch secret", "secretName", secretName) + + // Rollback to the previous hash and the previous last successful sync time. + ss.Status.SyncHash = prevSecretHash + ss.Status.LastSuccessfulSyncTime = prevTime + + // Reset the create or update conditions + meta.RemoveStatusCondition(&ss.Status.Conditions, ConditionTypeCreate) + meta.RemoveStatusCondition(&ss.Status.Conditions, ConditionTypeUpdate) + + if checkIfErrorMessageCanBeDisplayed(err.Error()) { + failedCondition.Message = err.Error() + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonValidatingAdmissionPolicyCheckFailed, true) + } else { + r.updateStatusConditions(ctx, ss, ConditionTypeUnknown, conditionType, ConditionReasonSecretPatchFailedUnknownError, true) + } + + return ctrl.Result{}, err + } + + // No errors found, remove the failed conditions. + for _, cond := range ss.Status.Conditions { + if slices.Contains(FailedConditionsTriggeringRetry, cond.Reason) { + meta.RemoveStatusCondition(&ss.Status.Conditions, cond.Type) + } + } + + // Update the status. + err = r.Client.Status().Update(ctx, ss) + if err != nil { + return ctrl.Result{}, err + } + + logger.V(4).Info("Done... updated status", "syncHash", syncHash, "lastSuccessfulSyncTime", ss.Status.LastSuccessfulSyncTime) + return ctrl.Result{}, nil +} + +// checkIfErrorMessageCanBeDisplayed checks if the error message can be displayed in the condition message +// based on the allowed strings to display condition error message defined in the conditions.go file. +func checkIfErrorMessageCanBeDisplayed(errorMessage string) bool { + for _, allowedString := range AllowedStringsToDisplayConditionErrorMessage { + if strings.Contains(strings.ToLower(errorMessage), allowedString) { + return true + } + } + return false +} + +// serverSidePatchSecret performs a server-side patch on a Kubernetes Secret. +// It updates the specified secret with the provided data, labels, and annotations. +func (r *SecretSyncReconciler) serverSidePatchSecret(ctx context.Context, ss *secretsyncv1alpha1.SecretSync, name, namespace string, datamap map[string][]byte, labels, annotations map[string]string, secretType corev1.SecretType) (err error) { + secretKind := "Secret" + secretVersion := "v1" + + // Construct the patch for updating the Secret. + secretPatchData := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: secretKind, + APIVersion: secretVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ss.APIVersion, + Kind: ss.Kind, + Name: ss.Name, + UID: ss.UID, + }, + }, + }, + Data: datamap, + Type: secretType, + } + + patchData, err := json.Marshal(secretPatchData) + if err != nil { + return err + } + + // Perform the server-side patch on the Secret. + _, err = r.Clientset.CoreV1().Secrets(namespace).Patch(ctx, name, types.ApplyPatchType, patchData, metav1.PatchOptions{FieldManager: SecretSyncControllerFieldManager}) + if err != nil { + return err + } + + return nil +} + +// computeSecretDataObjectHash computes the HMAC hash of the provided secret data +// using the SS UID as the key. +func (r *SecretSyncReconciler) computeSecretDataObjectHash(secretData map[string][]byte, spc *secretsstorecsiv1.SecretProviderClass, ss *secretsyncv1alpha1.SecretSync) (string, error) { + // Serialize the secret data, parts of the spc and the ss data. + secretBytes, err := json.Marshal(secretData) + if err != nil { + return "", err + } + + spcBytesUID, err := json.Marshal(spc.UID) + if err != nil { + return "", err + } + secretBytes = append(secretBytes, spcBytesUID...) + + spcBytesGeneration, err := json.Marshal(spc.ObjectMeta.Generation) + if err != nil { + return "", err + } + secretBytes = append(secretBytes, spcBytesGeneration...) + + ssBytesUID, err := json.Marshal(ss.UID) + if err != nil { + return "", err + } + secretBytes = append(secretBytes, ssBytesUID...) + + ssBytesGeneration, err := json.Marshal(ss.ObjectMeta.Generation) + if err != nil { + return "", err + } + secretBytes = append(secretBytes, ssBytesGeneration...) + + ssBytesForceSync, err := json.Marshal(ss.Spec.ForceSynchronization) + if err != nil { + return "", err + } + secretBytes = append(secretBytes, ssBytesForceSync...) + + salt := []byte(string(ss.UID)) + dk := pbkdf2.Key(secretBytes, salt, 100_000, 32, sha512.New) + + // Create a new HMAC instance with SHA-56 as the hash type and the pbkdf2 key. + hmac := hmac.New(sha512.New, dk) + + _, err = hmac.Write(dk) + if err != nil { + return "", err + } + + // Get the final HMAC hash in hexadecimal format. + dataHmac := hmac.Sum(nil) + dataHmac = append([]byte(Version), dataHmac...) + hmacHex := hex.EncodeToString(dataHmac) + + return hmacHex, nil +} + +// processIfSecretChanged checks if the secret sync object has changed. +func (r *SecretSyncReconciler) processIfSecretChanged(oldObj, newObj client.Object) bool { + ssOldObj := oldObj.(*secretsyncv1alpha1.SecretSync) + ssNewObj := newObj.(*secretsyncv1alpha1.SecretSync) + + return ssNewObj.Status.SyncHash != ssOldObj.Status.SyncHash +} + +// We need to trigger the reconcile function when the secret sync object is created or updated, however +// we don't need to trigger the reconcile function when the status of the secret sync object is updated. +func (r *SecretSyncReconciler) shouldReconcilePredicate() predicate.Funcs { + return predicate.Funcs{ + CreateFunc: func(_ event.CreateEvent) bool { + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return r.processIfSecretChanged(e.ObjectOld, e.ObjectNew) + }, + DeleteFunc: func(_ event.DeleteEvent) bool { + return false + }, + GenericFunc: func(_ event.GenericEvent) bool { + return true + }, + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *SecretSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&secretsyncv1alpha1.SecretSync{}). + WithEventFilter(r.shouldReconcilePredicate()). + Complete(r) +} diff --git a/secret-sync-controller/controllers/suite_test.go b/secret-sync-controller/controllers/suite_test.go new file mode 100644 index 000000000..e908d0b50 --- /dev/null +++ b/secret-sync-controller/controllers/suite_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + secretsyncv1alpha1 "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/api/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = secretsyncv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/secret-sync-controller/go.mod b/secret-sync-controller/go.mod new file mode 100644 index 000000000..c35944ba2 --- /dev/null +++ b/secret-sync-controller/go.mod @@ -0,0 +1,88 @@ +module sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller + +go 1.21 + +toolchain go1.21.6 + +require ( + github.com/google/go-cmp v0.6.0 + github.com/onsi/ginkgo/v2 v2.13.0 + github.com/onsi/gomega v1.28.1 + github.com/prometheus/client_golang v1.16.0 + go.opentelemetry.io/otel/exporters/prometheus v0.38.1 + go.opentelemetry.io/otel/metric v0.38.1 + go.opentelemetry.io/otel/sdk/metric v0.38.1 + golang.org/x/crypto v0.17.0 + google.golang.org/grpc v1.59.0 + google.golang.org/protobuf v1.31.0 + k8s.io/api v0.28.3 + k8s.io/apimachinery v0.28.3 + k8s.io/client-go v0.28.3 + k8s.io/klog/v2 v2.100.1 + k8s.io/utils v0.0.0-20240310230437-4693a0247e57 + sigs.k8s.io/controller-runtime v0.16.3 + sigs.k8s.io/secrets-store-csi-driver v1.4.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // 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/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/uuid v1.3.1 // 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/matttproud/golang_protobuf_extensions v1.0.4 // 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/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.opentelemetry.io/otel v1.15.1 // indirect + go.opentelemetry.io/otel/sdk v1.15.1 // indirect + go.opentelemetry.io/otel/trace v1.15.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.25.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.11.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.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/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // 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.3 // indirect + k8s.io/component-base v0.28.3 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // 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/secret-sync-controller/go.sum b/secret-sync-controller/go.sum new file mode 100644 index 000000000..ee5fb7879 --- /dev/null +++ b/secret-sync-controller/go.sum @@ -0,0 +1,257 @@ +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.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.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.2/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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/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.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= +github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +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.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/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.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +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.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= +go.opentelemetry.io/otel/exporters/prometheus v0.38.1 h1:GwalIvFIx91qIA8qyAyqYj9lql5Ba2Oxj/jDG6+3UoU= +go.opentelemetry.io/otel/exporters/prometheus v0.38.1/go.mod h1:6K7aBvWHXRUcNYFSj6Hi5hHwzA1jYflG/T8snrX4dYM= +go.opentelemetry.io/otel/metric v0.38.1 h1:2MM7m6wPw9B8Qv8iHygoAgkbejed59uUR6ezR5T3X2s= +go.opentelemetry.io/otel/metric v0.38.1/go.mod h1:FwqNHD3I/5iX9pfrRGZIlYICrJv0rHEUl2Ln5vdIVnQ= +go.opentelemetry.io/otel/sdk v1.15.1 h1:5FKR+skgpzvhPQHIEfcwMYjCBr14LWzs3uSqKiQzETI= +go.opentelemetry.io/otel/sdk v1.15.1/go.mod h1:8rVtxQfrbmbHKfqzpQkT5EzZMcbMBwTzNAggbEAM0KA= +go.opentelemetry.io/otel/sdk/metric v0.38.1 h1:EkO5wI4NT/fUaoPMGc0fKV28JaWe7q4vfVpEVasGb+8= +go.opentelemetry.io/otel/sdk/metric v0.38.1/go.mod h1:Rn4kSXFF9ZQZ5lL1pxQjCbK4seiO+U7s0ncmIFJaj34= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= +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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +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.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +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-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= +k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +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/secrets-store-csi-driver v1.4.1 h1:B198BKkfd2XC4itggSKzRpdBiG9ydbmcjFoJHZj3xXI= +sigs.k8s.io/secrets-store-csi-driver v1.4.1/go.mod h1:ZUdzEpDMuT6mtXzRUppfkSmyKSwVRNt0kYE92g2FGtE= +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/secret-sync-controller/hack/boilerplate.go.txt b/secret-sync-controller/hack/boilerplate.go.txt new file mode 100644 index 000000000..4ad438571 --- /dev/null +++ b/secret-sync-controller/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ \ No newline at end of file diff --git a/secret-sync-controller/main.go b/secret-sync-controller/main.go new file mode 100644 index 000000000..f1af65d8b --- /dev/null +++ b/secret-sync-controller/main.go @@ -0,0 +1,144 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 main + +import ( + "flag" + "os" + "strings" + "time" + + "google.golang.org/grpc" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + secretsstorecsiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + secretsyncv1alpha1 "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/api/v1alpha1" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/controllers" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/pkg/k8s" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/pkg/provider" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(secretsyncv1alpha1.AddToScheme(scheme)) + + utilruntime.Must(secretsstorecsiv1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var tokenRequestAudiences string + var providerVolumePath string + var maxCallRecvMsgSize int + + var rotationPollInterval *time.Duration = flag.Duration("rotation-poll-interval", time.Duration(21600)*time.Second, "Secret rotation poll interval duration") + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8085", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.StringVar(&providerVolumePath, "provider-volume", "/provider", "Volume path for provider.") + flag.IntVar(&maxCallRecvMsgSize, "max-call-recv-msg-size", 1024*1024*4, "maximum size in bytes of gRPC response from plugins") + flag.StringVar(&tokenRequestAudiences, "token-request-audience", "", "Audience for the token request, comma separated.") + + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: server.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "29f1d54e.secret-sync.x-k8s.io", + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + // token request client + kubeClient := kubernetes.NewForConfigOrDie(ctrl.GetConfigOrDie()) + tokenClient := k8s.NewTokenClient(kubeClient) + if err != nil { + setupLog.Error(err, "failed to create token client") + os.Exit(1) + } + + providerClients := provider.NewPluginClientBuilder([]string{providerVolumePath}, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize))) + defer providerClients.Cleanup() + + audiences := strings.Split(tokenRequestAudiences, ",") + if len(tokenRequestAudiences) == 0 { + audiences = []string{} + } + + if err = (&controllers.SecretSyncReconciler{ + Clientset: kubeClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + TokenClient: tokenClient, + ProviderClients: providerClients, + Audiences: audiences, + RotationPollInterval: *rotationPollInterval, + EventRecorder: record.NewBroadcaster().NewRecorder(scheme, corev1.EventSource{Component: "secret-sync-controller"}), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SecretSync") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/secret-sync-controller/pkg/fakeprovider/fake_provider_client.go b/secret-sync-controller/pkg/fakeprovider/fake_provider_client.go new file mode 100644 index 000000000..096723486 --- /dev/null +++ b/secret-sync-controller/pkg/fakeprovider/fake_provider_client.go @@ -0,0 +1,172 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 fakeprovider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" + + "google.golang.org/grpc" +) + +type MockCSIDriverProviderClient struct { + socketPath string + returnErr error + errorCode string + objects []*v1alpha1.ObjectVersion + files []*v1alpha1.File +} + +// NewMocKCSIDriverProviderClient returns a mock csi-provider grpc server +func NewMocKCSIDriverProviderClient(socketPath string) (*MockCSIDriverProviderClient, error) { + s := &MockCSIDriverProviderClient{ + socketPath: socketPath, + } + return s, nil +} + +type MockPluginClientBuilder struct { + clients map[string]v1alpha1.CSIDriverProviderClient + socketPaths []string + lock sync.RWMutex +} + +func NewPluginClientBuilder(paths []string) *MockPluginClientBuilder { + pcb := &MockPluginClientBuilder{ + clients: make(map[string]v1alpha1.CSIDriverProviderClient), + socketPaths: paths, + lock: sync.RWMutex{}, + } + return pcb +} + +// SetReturnError sets expected error +func (m *MockCSIDriverProviderClient) SetReturnError(err error) { + m.returnErr = err +} + +// SetObjects sets expected objects id and version +func (m *MockCSIDriverProviderClient) SetObjects(objects map[string]string) { + ov := make([]*v1alpha1.ObjectVersion, 0, len(objects)) + for k, v := range objects { + ov = append(ov, &v1alpha1.ObjectVersion{Id: k, Version: v}) + } + m.objects = ov +} + +// SetFiles sets provider files to return on Mount +func (m *MockCSIDriverProviderClient) SetFiles(files []*v1alpha1.File) { + ov := make([]*v1alpha1.File, 0, len(files)) + for _, v := range files { + ov = append(ov, &v1alpha1.File{ + Path: v.Path, + Mode: v.Mode, + Contents: v.Contents, + }) + } + m.files = ov +} + +// SetProviderErrorCode sets provider error code to return +func (m *MockCSIDriverProviderClient) SetProviderErrorCode(errorCode string) { + m.errorCode = errorCode +} + +// Mount implements provider csi-provider method +func (m *MockCSIDriverProviderClient) Mount(_ context.Context, req *v1alpha1.MountRequest, _ ...grpc.CallOption) (*v1alpha1.MountResponse, error) { + var attrib, secret map[string]string + var filePermission os.FileMode + var err error + + if m.returnErr != nil { + return &v1alpha1.MountResponse{}, m.returnErr + } + if err = json.Unmarshal([]byte(req.GetAttributes()), &attrib); err != nil { + return nil, fmt.Errorf("failed to unmarshal attributes, error: %w", err) + } + if err = json.Unmarshal([]byte(req.GetSecrets()), &secret); err != nil { + return nil, fmt.Errorf("failed to unmarshal secrets, error: %w", err) + } + if err = json.Unmarshal([]byte(req.GetPermission()), &filePermission); err != nil { + return nil, fmt.Errorf("failed to unmarshal file permission, error: %w", err) + } + + return &v1alpha1.MountResponse{ + ObjectVersion: m.objects, + Error: &v1alpha1.Error{ + Code: m.errorCode, + }, + Files: m.files, + }, nil +} + +// Version implements provider csi-provider method +func (m *MockCSIDriverProviderClient) Version(_ context.Context, _ *v1alpha1.VersionRequest, _ ...grpc.CallOption) (*v1alpha1.VersionResponse, error) { + return &v1alpha1.VersionResponse{ + Version: "v1alpha1", + RuntimeName: "fakeprovider", + RuntimeVersion: "0.0.10", + }, nil +} + +// Get returns a CSIDriverProviderClient for the provider. If an existing client +// is not found a new one will be created and added to the MockPluginClientBuilder. +func (p *MockPluginClientBuilder) Get(_ context.Context, provider string) (v1alpha1.CSIDriverProviderClient, error) { + var out v1alpha1.CSIDriverProviderClient + + // load a client, + p.lock.RLock() + out, ok := p.clients[provider] + p.lock.RUnlock() + if ok { + return out, nil + } + + // check all paths + socketPath := filepath.Join(p.socketPaths[0], provider+".sock") + if socketPath == "" { + return nil, fmt.Errorf("%w: provider %q", errors.New("provider not found"), provider) + } + + out, err := NewMocKCSIDriverProviderClient(socketPath) + if err != nil { + return nil, err + } + + p.lock.Lock() + defer p.lock.Unlock() + + // retry reading from the map in case a concurrent Get(provider) succeeded + // and added a connection to the map before p.lock.Lock() was acquired. + if r, ok := p.clients[provider]; ok { + out = r + } else { + p.clients[provider] = out + } + return out, nil +} + +func (p *MockPluginClientBuilder) Set(ProviderClient v1alpha1.CSIDriverProviderClient, provider string) { + p.clients[provider] = ProviderClient +} diff --git a/secret-sync-controller/pkg/k8s/token.go b/secret-sync-controller/pkg/k8s/token.go new file mode 100644 index 000000000..9c78e56c8 --- /dev/null +++ b/secret-sync-controller/pkg/k8s/token.go @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Kubernetes Authors. + +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 k8s + +import ( + "encoding/json" + + authenticationv1 "k8s.io/api/authentication/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/pkg/k8s/token" +) + +// TokenClient is a client for Kubernetes Token API +type TokenClient struct { + manager *token.Manager +} + +// NewTokenClient creates a new TokenClient +// The client will be used to request a token for token audiences configured in the Secret Sync Controller. +func NewTokenClient(kubeClient kubernetes.Interface) *TokenClient { + return &TokenClient{ + manager: token.NewManager(kubeClient), + } +} + +// SecretProviderServiceAccountTokenAttrs returns the token for the federated service account that can be bound to the pod. +// This token will be sent to the providers and is of the format: +// +// "csi.storage.k8s.io/serviceAccount.tokens": { +// : { +// 'token': , +// 'expirationTimestamp': , +// }, +// ... +// } +// +// ref: https://kubernetes-csi.github.io/docs/token-requests.html#usage +func (c *TokenClient) SecretProviderServiceAccountTokenAttrs(namespace, serviceAccountName string, audiences []string) (map[string]string, error) { + + if len(audiences) == 0 { + return nil, nil + } + + outputs := map[string]authenticationv1.TokenRequestStatus{} + var tokenExpirationSeconds int64 = 600 + + for _, aud := range audiences { + tr := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: &tokenExpirationSeconds, + Audiences: []string{aud}, + }, + } + + tr, err := c.GetServiceAccountToken(namespace, serviceAccountName, tr) + if err != nil { + return nil, err + } + outputs[aud] = tr.Status + } + + klog.V(5).InfoS("Fetched service account token attrs", "serviceAccountName", serviceAccountName, "namespace", namespace) + tokens, err := json.Marshal(outputs) + if err != nil { + return nil, err + } + + return map[string]string{ + "csi.storage.k8s.io/serviceAccount.tokens": string(tokens), + }, nil +} + +// GetServiceAccountToken gets a service account token for a pod from cache or +// from the TokenRequest API. This process is as follows: +// * Check the cache for the current token request. +// * If the token exists and does not require a refresh, return the current token. +// * Attempt to refresh the token. +// * If the token is refreshed successfully, save it in the cache and return the token. +// * If refresh fails and the old token is still valid, log an error and return the old token. +// * If refresh fails and the old token is no longer valid, return an error +func (c *TokenClient) GetServiceAccountToken(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return c.manager.GetServiceAccountToken(namespace, name, tr) +} + +// DeleteServiceAccountToken should be invoked when pod got deleted. It simply +// clean token manager cache. +func (c *TokenClient) DeleteServiceAccountToken(podUID types.UID) { + c.manager.DeleteServiceAccountToken(podUID) +} diff --git a/secret-sync-controller/pkg/k8s/token/token_manager.go b/secret-sync-controller/pkg/k8s/token/token_manager.go new file mode 100644 index 000000000..88e4a3b34 --- /dev/null +++ b/secret-sync-controller/pkg/k8s/token/token_manager.go @@ -0,0 +1,213 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +// Vendored from kubernetes/pkg/kubelet/token/token_manager.go +// * tag: v1.25.3, +// * commit: 53ce79a18ab2665488f7c55c6a1cab8e7a09aced +// * link: https://github.com/kubernetes/kubernetes/blob/53ce79a18ab2665488f7c55c6a1cab8e7a09aced/pkg/kubelet/token/token_manager.go + +// Package token implements a manager of serviceaccount tokens for pods running +// on the node. +package token + +import ( + "context" + "errors" + "fmt" + "math/rand" + "sync" + "time" + + authenticationv1 "k8s.io/api/authentication/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + "k8s.io/utils/clock" +) + +const ( + maxTTL = 24 * time.Hour + gcPeriod = time.Minute + maxJitter = 10 * time.Second +) + +// NewManager returns a new token manager. +func NewManager(c clientset.Interface) *Manager { + // check whether the server supports token requests so we can give a more helpful error message + supported := false + once := &sync.Once{} + tokenRequestsSupported := func() bool { + once.Do(func() { + resources, err := c.Discovery().ServerResourcesForGroupVersion("v1") + if err != nil { + return + } + for idx := range resources.APIResources { + resource := &resources.APIResources[idx] + if resource.Name == "serviceaccounts/token" { + supported = true + return + } + } + }) + return supported + } + + m := &Manager{ + getToken: func(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + if c == nil { + return nil, errors.New("cannot use TokenManager when kubelet is in standalone mode") + } + tokenRequest, err := c.CoreV1().ServiceAccounts(namespace).CreateToken(context.TODO(), name, tr, metav1.CreateOptions{}) + if apierrors.IsNotFound(err) && !tokenRequestsSupported() { + return nil, fmt.Errorf("the API server does not have TokenRequest endpoints enabled") + } + return tokenRequest, err + }, + cache: make(map[string]*authenticationv1.TokenRequest), + clock: clock.RealClock{}, + } + go wait.Forever(m.cleanup, gcPeriod) + return m +} + +// Manager manages service account tokens for pods. +type Manager struct { + + // cacheMutex guards the cache + cacheMutex sync.RWMutex + cache map[string]*authenticationv1.TokenRequest + + // mocked for testing + getToken func(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) + clock clock.Clock +} + +// GetServiceAccountToken gets a service account token for a pod from cache or +// from the TokenRequest API. This process is as follows: +// * Check the cache for the current token request. +// * If the token exists and does not require a refresh, return the current token. +// * Attempt to refresh the token. +// * If the token is refreshed successfully, save it in the cache and return the token. +// * If refresh fails and the old token is still valid, log an error and return the old token. +// * If refresh fails and the old token is no longer valid, return an error +func (m *Manager) GetServiceAccountToken(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + key := keyFunc(name, namespace, tr) + + ctr, ok := m.get(key) + + if ok && !m.requiresRefresh(ctr) { + return ctr, nil + } + + tr, err := m.getToken(name, namespace, tr) + if err != nil { + switch { + case !ok: + return nil, fmt.Errorf("failed to fetch token: %w", err) + case m.expired(ctr): + return nil, fmt.Errorf("token %s expired and refresh failed: %w", key, err) + default: + klog.ErrorS(err, "Couldn't update token", "cacheKey", key) + return ctr, nil + } + } + + m.set(key, tr) + return tr, nil +} + +// DeleteServiceAccountToken should be invoked when pod got deleted. It simply +// clean token manager cache. +func (m *Manager) DeleteServiceAccountToken(podUID types.UID) { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + for k, tr := range m.cache { + if tr.Spec.BoundObjectRef.UID == podUID { + delete(m.cache, k) + } + } +} + +func (m *Manager) cleanup() { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + for k, tr := range m.cache { + if m.expired(tr) { + delete(m.cache, k) + } + } +} + +func (m *Manager) get(key string) (*authenticationv1.TokenRequest, bool) { + m.cacheMutex.RLock() + defer m.cacheMutex.RUnlock() + ctr, ok := m.cache[key] + return ctr, ok +} + +func (m *Manager) set(key string, tr *authenticationv1.TokenRequest) { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + m.cache[key] = tr +} + +func (m *Manager) expired(t *authenticationv1.TokenRequest) bool { + return m.clock.Now().After(t.Status.ExpirationTimestamp.Time) +} + +// requiresRefresh returns true if the token is older than 80% of its total +// ttl, or if the token is older than 24 hours. +func (m *Manager) requiresRefresh(tr *authenticationv1.TokenRequest) bool { + if tr.Spec.ExpirationSeconds == nil { + cpy := tr.DeepCopy() + cpy.Status.Token = "" + klog.ErrorS(nil, "Expiration seconds was nil for token request", "tokenRequest", cpy) + return false + } + now := m.clock.Now() + exp := tr.Status.ExpirationTimestamp.Time + iat := exp.Add(-1 * time.Duration(*tr.Spec.ExpirationSeconds) * time.Second) + + // #nosec G404: Use of weak random number generator (math/rand instead of crypto/rand) + jitter := time.Duration(rand.Float64()*maxJitter.Seconds()) * time.Second + if now.After(iat.Add(maxTTL - jitter)) { + return true + } + // Require a refresh if within 20% of the TTL plus a jitter from the expiration time. + if now.After(exp.Add(-1*time.Duration((*tr.Spec.ExpirationSeconds*20)/100)*time.Second - jitter)) { + return true + } + return false +} + +// keys should be nonconfidential and safe to log +func keyFunc(name, namespace string, tr *authenticationv1.TokenRequest) string { + var exp int64 + if tr.Spec.ExpirationSeconds != nil { + exp = *tr.Spec.ExpirationSeconds + } + + var ref authenticationv1.BoundObjectReference + if tr.Spec.BoundObjectRef != nil { + ref = *tr.Spec.BoundObjectRef + } + + return fmt.Sprintf("%q/%q/%#v/%#v/%#v", name, namespace, tr.Spec.Audiences, exp, ref) +} diff --git a/secret-sync-controller/pkg/k8s/token/token_manager_test.go b/secret-sync-controller/pkg/k8s/token/token_manager_test.go new file mode 100644 index 000000000..6cbfb71bc --- /dev/null +++ b/secret-sync-controller/pkg/k8s/token/token_manager_test.go @@ -0,0 +1,610 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +// Vendored from kubernetes/pkg/kubelet/token/token_manager_test.go +// * tag: v1.25.3, +// * commit: 53ce79a18ab2665488f7c55c6a1cab8e7a09aced +// * link: https://github.com/kubernetes/kubernetes/blob/53ce79a18ab2665488f7c55c6a1cab8e7a09aced/pkg/kubelet/token/token_manager_test.go + +package token + +import ( + "fmt" + "testing" + "time" + + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + testingclock "k8s.io/utils/clock/testing" +) + +func TestTokenCachingAndExpiration(t *testing.T) { + type suite struct { + clock *testingclock.FakeClock + tg *fakeTokenGetter + mgr *Manager + } + + cases := []struct { + name string + exp time.Duration + f func(t *testing.T, s *suite) + }{ + { + name: "rotate hour token expires in the last 12 minutes", + exp: time.Hour, + f: func(t *testing.T, s *suite) { + s.clock.SetTime(s.clock.Now().Add(50 * time.Minute)) + if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 2 { + t.Fatalf("expected token to be refreshed: call count was %d", s.tg.count) + } + }, + }, + { + name: "rotate 24 hour token that expires in 40 hours", + exp: 40 * time.Hour, + f: func(t *testing.T, s *suite) { + s.clock.SetTime(s.clock.Now().Add(25 * time.Hour)) + if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 2 { + t.Fatalf("expected token to be refreshed: call count was %d", s.tg.count) + } + }, + }, + { + name: "rotate hour token fails, old token is still valid, doesn't error", + exp: time.Hour, + f: func(t *testing.T, s *suite) { + s.clock.SetTime(s.clock.Now().Add(50 * time.Minute)) + tg := &fakeTokenGetter{ + err: fmt.Errorf("err"), + } + s.mgr.getToken = tg.getToken + tr, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tr.Status.Token != "foo" { + t.Fatalf("unexpected token: %v", tr.Status.Token) + } + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + clock := testingclock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) + expSecs := int64(c.exp.Seconds()) + s := &suite{ + clock: clock, + mgr: NewManager(nil), + tg: &fakeTokenGetter{ + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: &expSecs, + }, + Status: authenticationv1.TokenRequestStatus{ + Token: "foo", + ExpirationTimestamp: metav1.Time{Time: clock.Now().Add(c.exp)}, + }, + }, + }, + } + s.mgr.getToken = s.tg.getToken + s.mgr.clock = s.clock + if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 1 { + t.Fatalf("unexpected client call, got: %d, want: 1", s.tg.count) + } + + if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 1 { + t.Fatalf("expected token to be served from cache: saw %d", s.tg.count) + } + + c.f(t, s) + }) + } +} + +func TestRequiresRefresh(t *testing.T) { + start := time.Now() + cases := []struct { + now, exp time.Time + expectRefresh bool + requestTweaks func(*authenticationv1.TokenRequest) + }{ + { + now: start.Add(10 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: false, + }, + { + now: start.Add(50 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: true, + }, + { + now: start.Add(25 * time.Hour), + exp: start.Add(60 * time.Hour), + expectRefresh: true, + }, + { + now: start.Add(70 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: true, + }, + { + // expiry will be overwritten by the tweak below. + now: start.Add(0 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: false, + requestTweaks: func(tr *authenticationv1.TokenRequest) { + tr.Spec.ExpirationSeconds = nil + }, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprint(i), func(t *testing.T) { + clock := testingclock.NewFakeClock(c.now) + secs := int64(c.exp.Sub(start).Seconds()) + tr := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: &secs, + }, + Status: authenticationv1.TokenRequestStatus{ + ExpirationTimestamp: metav1.Time{Time: c.exp}, + }, + } + + if c.requestTweaks != nil { + c.requestTweaks(tr) + } + + mgr := NewManager(nil) + mgr.clock = clock + + rr := mgr.requiresRefresh(tr) + if rr != c.expectRefresh { + t.Fatalf("unexpected requiresRefresh result, got: %v, want: %v", rr, c.expectRefresh) + } + }) + } +} + +func TestDeleteServiceAccountToken(t *testing.T) { + type request struct { + name, namespace string + tr authenticationv1.TokenRequest + shouldFail bool + } + + cases := []struct { + name string + requestIndex []int + deletePodUID []types.UID + expLeftIndex []int + }{ + { + name: "delete none with all success requests", + requestIndex: []int{0, 1, 2}, + expLeftIndex: []int{0, 1, 2}, + }, + { + name: "delete one with all success requests", + requestIndex: []int{0, 1, 2}, + deletePodUID: []types.UID{"fake-uid-1"}, + expLeftIndex: []int{1, 2}, + }, + { + name: "delete two with all success requests", + requestIndex: []int{0, 1, 2}, + deletePodUID: []types.UID{"fake-uid-1", "fake-uid-3"}, + expLeftIndex: []int{1}, + }, + { + name: "delete all with all suceess requests", + requestIndex: []int{0, 1, 2}, + deletePodUID: []types.UID{"fake-uid-1", "fake-uid-2", "fake-uid-3"}, + }, + { + name: "delete no pod with failed requests", + requestIndex: []int{0, 1, 2, 3}, + deletePodUID: []types.UID{}, + expLeftIndex: []int{0, 1, 2}, + }, + { + name: "delete other pod with failed requests", + requestIndex: []int{0, 1, 2, 3}, + deletePodUID: []types.UID{"fake-uid-2"}, + expLeftIndex: []int{0, 2}, + }, + { + name: "delete no pod with request which success after failure", + requestIndex: []int{0, 1, 2, 3, 4}, + deletePodUID: []types.UID{}, + expLeftIndex: []int{0, 1, 2, 4}, + }, + { + name: "delete the pod which success after failure", + requestIndex: []int{0, 1, 2, 3, 4}, + deletePodUID: []types.UID{"fake-uid-4"}, + expLeftIndex: []int{0, 1, 2}, + }, + { + name: "delete other pod with request which success after failure", + requestIndex: []int{0, 1, 2, 3, 4}, + deletePodUID: []types.UID{"fake-uid-1"}, + expLeftIndex: []int{1, 2, 4}, + }, + { + name: "delete some pod not in the set", + requestIndex: []int{0, 1, 2}, + deletePodUID: []types.UID{"fake-uid-100", "fake-uid-200"}, + expLeftIndex: []int{0, 1, 2}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + requests := []request{ + { + name: "fake-name-1", + namespace: "fake-namespace-1", + tr: authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + BoundObjectRef: &authenticationv1.BoundObjectReference{ + UID: "fake-uid-1", + Name: "fake-name-1", + }, + }, + }, + shouldFail: false, + }, + { + name: "fake-name-2", + namespace: "fake-namespace-2", + tr: authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + BoundObjectRef: &authenticationv1.BoundObjectReference{ + UID: "fake-uid-2", + Name: "fake-name-2", + }, + }, + }, + shouldFail: false, + }, + { + name: "fake-name-3", + namespace: "fake-namespace-3", + tr: authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + BoundObjectRef: &authenticationv1.BoundObjectReference{ + UID: "fake-uid-3", + Name: "fake-name-3", + }, + }, + }, + shouldFail: false, + }, + { + name: "fake-name-4", + namespace: "fake-namespace-4", + tr: authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + BoundObjectRef: &authenticationv1.BoundObjectReference{ + UID: "fake-uid-4", + Name: "fake-name-4", + }, + }, + }, + shouldFail: true, + }, + { + // exactly the same with last one, besides it will success + name: "fake-name-4", + namespace: "fake-namespace-4", + tr: authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + BoundObjectRef: &authenticationv1.BoundObjectReference{ + UID: "fake-uid-4", + Name: "fake-name-4", + }, + }, + }, + shouldFail: false, + }, + } + testMgr := NewManager(nil) + testMgr.clock = testingclock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) + + successGetToken := func(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + tr.Status = authenticationv1.TokenRequestStatus{ + ExpirationTimestamp: metav1.Time{Time: testMgr.clock.Now().Add(10 * time.Hour)}, + } + return tr, nil + } + failGetToken := func(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return nil, fmt.Errorf("fail tr") + } + + for _, index := range c.requestIndex { + req := requests[index] + if req.shouldFail { + testMgr.getToken = failGetToken + } else { + testMgr.getToken = successGetToken + } + _, _ = testMgr.GetServiceAccountToken(req.namespace, req.name, &req.tr) + } + + for _, uid := range c.deletePodUID { + testMgr.DeleteServiceAccountToken(uid) + } + if len(c.expLeftIndex) != len(testMgr.cache) { + t.Errorf("%s got unexpected result: expected left cache size is %d, got %d", c.name, len(c.expLeftIndex), len(testMgr.cache)) + } + for _, leftIndex := range c.expLeftIndex { + r := requests[leftIndex] + _, ok := testMgr.get(keyFunc(r.name, r.namespace, &r.tr)) + if !ok { + t.Errorf("%s got unexpected result: expected token request %v exist in cache, but not", c.name, r) + } + } + }) + } +} + +type fakeTokenGetter struct { + count int + tr *authenticationv1.TokenRequest + err error +} + +func (ftg *fakeTokenGetter) getToken(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + ftg.count++ + return ftg.tr, ftg.err +} + +func TestCleanup(t *testing.T) { + cases := []struct { + name string + relativeExp time.Duration + expectedCacheSize int + }{ + { + name: "don't cleanup unexpired tokens", + relativeExp: -1 * time.Hour, + expectedCacheSize: 0, + }, + { + name: "cleanup expired tokens", + relativeExp: time.Hour, + expectedCacheSize: 1, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + clock := testingclock.NewFakeClock(time.Time{}.Add(24 * time.Hour)) + mgr := NewManager(nil) + mgr.clock = clock + + mgr.set("key", &authenticationv1.TokenRequest{ + Status: authenticationv1.TokenRequestStatus{ + ExpirationTimestamp: metav1.Time{Time: mgr.clock.Now().Add(c.relativeExp)}, + }, + }) + mgr.cleanup() + if got, want := len(mgr.cache), c.expectedCacheSize; got != want { + t.Fatalf("unexpected number of cache entries after cleanup, got: %d, want: %d", got, want) + } + }) + } +} + +func TestKeyFunc(t *testing.T) { + type tokenRequestUnit struct { + name string + namespace string + tr *authenticationv1.TokenRequest + } + getKeyFunc := func(u tokenRequestUnit) string { + return keyFunc(u.name, u.namespace, u.tr) + } + + cases := []struct { + name string + trus []tokenRequestUnit + target tokenRequestUnit + + shouldHit bool + }{ + { + name: "hit", + trus: []tokenRequestUnit{ + { + name: "foo-sa", + namespace: "foo-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "foo-pod", + UID: "foo-uid", + }, + }, + }, + }, + { + name: "ame-sa", + namespace: "ame-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"ame1", "ame2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "ame-pod", + UID: "ame-uid", + }, + }, + }, + }, + }, + target: tokenRequestUnit{ + name: "foo-sa", + namespace: "foo-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "foo-pod", + UID: "foo-uid", + }, + }, + }, + }, + shouldHit: true, + }, + { + name: "not hit due to different ExpirationSeconds", + trus: []tokenRequestUnit{ + { + name: "foo-sa", + namespace: "foo-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "foo-pod", + UID: "foo-uid", + }, + }, + }, + }, + }, + target: tokenRequestUnit{ + name: "foo-sa", + namespace: "foo-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + // everything is same besides ExpirationSeconds + ExpirationSeconds: getInt64Point(2001), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "foo-pod", + UID: "foo-uid", + }, + }, + }, + }, + shouldHit: false, + }, + { + name: "not hit due to different BoundObjectRef", + trus: []tokenRequestUnit{ + { + name: "foo-sa", + namespace: "foo-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "foo-pod", + UID: "foo-uid", + }, + }, + }, + }, + }, + target: tokenRequestUnit{ + name: "foo-sa", + namespace: "foo-ns", + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + // everything is same besides BoundObjectRef.Name + Name: "diff-pod", + UID: "foo-uid", + }, + }, + }, + }, + shouldHit: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + mgr := NewManager(nil) + mgr.clock = testingclock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) + for _, tru := range c.trus { + mgr.set(getKeyFunc(tru), &authenticationv1.TokenRequest{ + Status: authenticationv1.TokenRequestStatus{ + // make sure the token cache would not be cleaned by token manager cleanup func + ExpirationTimestamp: metav1.Time{Time: mgr.clock.Now().Add(50 * time.Minute)}, + }, + }) + } + _, hit := mgr.get(getKeyFunc(c.target)) + + if hit != c.shouldHit { + t.Errorf("%s got unexpected hit result: expected to be %t, got %t", c.name, c.shouldHit, hit) + } + }) + } +} + +func getTokenRequest() *authenticationv1.TokenRequest { + return &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"foo1", "foo2"}, + ExpirationSeconds: getInt64Point(2000), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "pod", + Name: "foo-pod", + UID: "foo-uid", + }, + }, + } +} + +func getInt64Point(v int64) *int64 { + return &v +} diff --git a/secret-sync-controller/pkg/k8s/token_test.go b/secret-sync-controller/pkg/k8s/token_test.go new file mode 100644 index 000000000..1b413902a --- /dev/null +++ b/secret-sync-controller/pkg/k8s/token_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2022 The Kubernetes Authors. + +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 k8s + +import ( + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + fakeclient "k8s.io/client-go/kubernetes/fake" + clitesting "k8s.io/client-go/testing" + "k8s.io/utils/ptr" +) + +var ( + testAccount = "test-service-account" + testNamespace = "test-ns" +) + +func TestSecretProviderServiceAccountTokenAttrs(t *testing.T) { + scheme := runtime.NewScheme() + audience := "aud" + + tests := []struct { + desc string + audiences []string + wantServiceAccountTokenAttrs map[string]string + }{ + { + desc: "no ServiceAccountToken", + audiences: []string{}, + wantServiceAccountTokenAttrs: nil, + }, + { + desc: "one token with empty string as audience", + audiences: []string{""}, + wantServiceAccountTokenAttrs: map[string]string{"csi.storage.k8s.io/serviceAccount.tokens": `{"":{"token":"test-ns:test-service-account:600:[]","expirationTimestamp":"1970-01-01T00:00:01Z"}}`}, + }, + { + desc: "one token with non-empty string as audience", + audiences: []string{audience}, + wantServiceAccountTokenAttrs: map[string]string{"csi.storage.k8s.io/serviceAccount.tokens": `{"aud":{"token":"test-ns:test-service-account:600:[aud]","expirationTimestamp":"1970-01-01T00:00:01Z"}}`}, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + client := fakeclient.NewSimpleClientset() + client.PrependReactor("create", "serviceaccounts", clitesting.ReactionFunc(func(action clitesting.Action) (bool, runtime.Object, error) { + tr := action.(clitesting.CreateAction).GetObject().(*authenticationv1.TokenRequest) + scheme.Default(tr) + if len(tr.Spec.Audiences) == 0 { + tr.Spec.Audiences = []string{} + } + tr.Spec.ExpirationSeconds = ptr.To[int64](600) + tr.Status.Token = fmt.Sprintf("%v:%v:%d:%v", action.GetNamespace(), testAccount, *tr.Spec.ExpirationSeconds, tr.Spec.Audiences) + tr.Status.ExpirationTimestamp = metav1.NewTime(time.Unix(1, 1)) + return true, tr, nil + })) + + tokenClient := NewTokenClient(client) + var attrs map[string]string + attrs, _ = tokenClient.SecretProviderServiceAccountTokenAttrs(testNamespace, testAccount, test.audiences) + if diff := cmp.Diff(test.wantServiceAccountTokenAttrs, attrs); diff != "" { + t.Errorf("PodServiceAccountTokenAttrs() returned diff (-want +got):\n%s", diff) + } + }) + } +} diff --git a/secret-sync-controller/pkg/metrics/exporter.go b/secret-sync-controller/pkg/metrics/exporter.go new file mode 100644 index 000000000..dd77a878c --- /dev/null +++ b/secret-sync-controller/pkg/metrics/exporter.go @@ -0,0 +1,27 @@ +package metrics + +import ( + "flag" + "fmt" + "strings" + + "k8s.io/klog/v2" +) + +var ( + metricsBackend = flag.String("metrics-backend", "Prometheus", "Backend used for metrics") +) + +const prometheusExporter = "prometheus" + +func InitMetricsExporter() error { + mb := strings.ToLower(*metricsBackend) + klog.InfoS("initializing metrics backend", "backend", mb) + switch mb { + // Prometheus is the only supported exporter + case prometheusExporter: + return initPrometheusExporter() + default: + return fmt.Errorf("unsupported metrics backend %v", *metricsBackend) + } +} diff --git a/secret-sync-controller/pkg/metrics/prometheus_exporter.go b/secret-sync-controller/pkg/metrics/prometheus_exporter.go new file mode 100644 index 000000000..18ac1d160 --- /dev/null +++ b/secret-sync-controller/pkg/metrics/prometheus_exporter.go @@ -0,0 +1,52 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 metrics + +import ( + crprometheus "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/metric/global" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/aggregation" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +func initPrometheusExporter() error { + exporter, err := prometheus.New( + prometheus.WithRegisterer(metrics.Registry.(*crprometheus.Registry)), // using the controller-runtime prometheus metrics registry + ) + if err != nil { + return err + } + + meterProvider := metric.NewMeterProvider( + metric.WithReader(exporter), + metric.WithView(metric.NewView( + metric.Instrument{Kind: metric.InstrumentKindHistogram}, + metric.Stream{ + Aggregation: aggregation.ExplicitBucketHistogram{ + // Use custom buckets to avoid the default buckets which are too small for our use case. + // Start 100ms with last bucket being [~4m, +Inf) + Boundaries: crprometheus.ExponentialBucketsRange(0.1, 2, 11), + }}, + )), + ) + + global.SetMeterProvider(meterProvider) + + return nil +} diff --git a/secret-sync-controller/pkg/provider/provider_client.go b/secret-sync-controller/pkg/provider/provider_client.go new file mode 100644 index 000000000..d2df6178e --- /dev/null +++ b/secret-sync-controller/pkg/provider/provider_client.go @@ -0,0 +1,309 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 provider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "sigs.k8s.io/secrets-store-csi-driver/pkg/util/runtimeutil" + "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "k8s.io/klog/v2" +) + +// ServiceConfig is used when building CSIDriverProvider clients. The configured +// retry parameters ensures that RPCs will be retried if the underlying +// connection is not ready. +// +// For more details see: +// https://github.com/grpc/grpc/blob/master/doc/service_config.md +const ServiceConfig = ` +{ + "methodConfig": [ + { + "name": [{"service": "v1alpha1.CSIDriverProvider"}], + "waitForReady": true, + "retryPolicy": { + "MaxAttempts": 3, + "InitialBackoff": "1s", + "MaxBackoff": "10s", + "BackoffMultiplier": 1.1, + "RetryableStatusCodes": [ "UNAVAILABLE" ] + } + } + ] +} +` + +var ( + // pluginNameRe is the regular expression used to validate plugin names. + pluginNameRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{0,30}$`) + + errInvalidProvider = errors.New("invalid provider") + errProviderNotFound = errors.New("provider not found") + errMissingObjectVersions = errors.New("missing object versions") +) + +// PluginClientBuilder builds and stores grpc clients for communicating with +// provider plugins. +type PluginClientBuilder struct { + clients map[string]v1alpha1.CSIDriverProviderClient + conns map[string]*grpc.ClientConn + socketPaths []string + lock sync.RWMutex + opts []grpc.DialOption +} + +// NewPluginClientBuilder creates a PluginClientBuilder that will connect to +// plugins in the provided absolute path to a folder. Plugin servers must listen +// to the unix domain socket at: +// +// /.sock +// +// where must match the PluginNameRe regular expression. +// +// Additional grpc dial options can also be set through opts and will be used +// when creating all clients. +func NewPluginClientBuilder(paths []string, opts ...grpc.DialOption) *PluginClientBuilder { + pcb := &PluginClientBuilder{ + clients: make(map[string]v1alpha1.CSIDriverProviderClient), + conns: make(map[string]*grpc.ClientConn), + socketPaths: paths, + lock: sync.RWMutex{}, + opts: append(opts, []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), // the interface is only secured through filesystem ACLs + grpc.WithContextDialer(func(ctx context.Context, target string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", target) + }), + grpc.WithDefaultServiceConfig(ServiceConfig), + }..., + ), + } + return pcb +} + +// Get returns a CSIDriverProviderClient for the provider. If an existing client +// is not found a new one will be created and added to the PluginClientBuilder. +func (p *PluginClientBuilder) Get(_ context.Context, provider string) (v1alpha1.CSIDriverProviderClient, error) { + var out v1alpha1.CSIDriverProviderClient + + // load a client, + p.lock.RLock() + out, ok := p.clients[provider] + p.lock.RUnlock() + if ok { + return out, nil + } + // client does not exist, create a new one + if !pluginNameRe.MatchString(provider) { + return nil, fmt.Errorf("%w: provider %q", errInvalidProvider, provider) + } + + // check all paths + socketPath := "" + for k := range p.socketPaths { + tryPath := filepath.Join(p.socketPaths[k], provider+".sock") + if _, err := os.Stat(tryPath); err == nil { + socketPath = tryPath + break + } + // TODO: This is a workaround for Windows 20H2 issue for os.Stat(). See + // microsoft/Windows-Containers#97 for details. + // Once the issue is resolved, the following os.Lstat() is not needed. + if runtimeutil.IsRuntimeWindows() { + if _, err := os.Lstat(tryPath); err == nil { + socketPath = tryPath + break + } + } + } + + if socketPath == "" { + return nil, fmt.Errorf("%w: provider %q", errProviderNotFound, provider) + } + + conn, err := grpc.Dial( + socketPath, + p.opts..., + ) + if err != nil { + return nil, err + } + out = v1alpha1.NewCSIDriverProviderClient(conn) + + p.lock.Lock() + defer p.lock.Unlock() + + // retry reading from the map in case a concurrent Get(provider) succeeded + // and added a connection to the map before p.lock.Lock() was acquired. + if r, ok := p.clients[provider]; ok { + out = r + } else { + p.conns[provider] = conn + p.clients[provider] = out + } + return out, nil +} + +// Cleanup closes all underlying connections and removes all clients. +func (p *PluginClientBuilder) Cleanup() { + p.lock.Lock() + defer p.lock.Unlock() + + for k := range p.conns { + if err := p.conns[k].Close(); err != nil { + klog.ErrorS(err, "error shutting down provider connection", "provider", k) + } + } + p.clients = make(map[string]v1alpha1.CSIDriverProviderClient) + p.conns = make(map[string]*grpc.ClientConn) +} + +// HealthCheck enables periodic healthcheck for configured provider clients by making +// a Version() RPC call. If the provider healthcheck fails, we log an error. +// +// This method blocks until the parent context is canceled during termination. +func (p *PluginClientBuilder) HealthCheck(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + p.lock.RLock() + + for provider, client := range p.clients { + c, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + runtimeVersion, err := Version(c, client) + if err != nil { + klog.V(5).ErrorS(err, "provider healthcheck failed", "provider", provider) + continue + } + klog.V(5).InfoS("provider healthcheck successful", "provider", provider, "runtimeVersion", runtimeVersion) + } + + p.lock.RUnlock() + } + } +} + +// MountContent calls the client's Mount() RPC with helpers to format the +// request and interpret the response. +func MountContent(ctx context.Context, client v1alpha1.CSIDriverProviderClient, attributes, secrets string, oldObjectVersions map[string]string) (map[string]string, map[string][]byte, error) { + objVersions := make([]*v1alpha1.ObjectVersion, 0, len(oldObjectVersions)) + for obj, version := range oldObjectVersions { + objVersions = append(objVersions, &v1alpha1.ObjectVersion{Id: obj, Version: version}) + } + + // TODO: permissions should be deprecated from the provider interface + permissionJSON, err := json.Marshal(os.FileMode(int(0644))) + if err != nil { + return nil, nil, err + } + + req := &v1alpha1.MountRequest{ + Attributes: attributes, + Secrets: secrets, + Permission: string(permissionJSON), + CurrentObjectVersion: objVersions, + } + + resp, err := client.Mount(ctx, req) + if err != nil { + if isMaxRecvMsgSizeError(err) { + klog.ErrorS(err, "Set --max-call-recv-msg-size to configure larger maximum size in bytes of gRPC response") + } + return nil, nil, err + } + klog.V(5).Info("finished mount request") + if resp != nil && resp.GetError() != nil && len(resp.GetError().Code) > 0 { + return nil, nil, fmt.Errorf("mount request failed with provider error code %s", resp.GetError().Code) + } + + ov := resp.GetObjectVersion() + if ov == nil { + return nil, nil, errMissingObjectVersions + } + objectVersions := make(map[string]string) + for _, v := range ov { + objectVersions[v.Id] = v.Version + } + + // warn if the proto response size is over 1 MiB. + // Individual k8s secrets are limited to 1MiB in size. + // Ref: https://kubernetes.io/docs/concepts/configuration/secret/#restrictions + if size := proto.Size(resp); size > 1048576 { + klog.InfoS("proto above 1MiB, secret sync may fail", "size", size) + } + + files := make(map[string][]byte, len(resp.GetFiles())) + for _, f := range resp.GetFiles() { + files[f.GetPath()] = f.GetContents() + } + return objectVersions, files, nil +} + +// Version calls the client's Version() RPC +// returns provider runtime version and error. +func Version(ctx context.Context, client v1alpha1.CSIDriverProviderClient) (string, error) { + req := &v1alpha1.VersionRequest{ + Version: "v1alpha1", + } + + resp, err := client.Version(ctx, req) + if err != nil { + return "", err + } + return resp.RuntimeVersion, nil +} + +// isMaxRecvMsgSizeError checks if the grpc error is of ResourceExhausted type and +// msg size is larger than max configured. +func isMaxRecvMsgSizeError(err error) bool { + if status.Code(err) != codes.ResourceExhausted { + return false + } + // ResourceExhausted errors are not exclusively related to --max-call-recv-msg-size and could also be the result of propagating quota errors. + // Skipping errors that are related to the machine limits + if strings.Contains(err.Error(), "grpc: received message larger than max length allowed on current machine") { + return false + } + // Skipping ResourceExhausted errors that are other than internal grpc system errors + if !strings.Contains(err.Error(), "grpc: received message larger than max") { + return false + } + return true +} diff --git a/secret-sync-controller/pkg/util/secretutil/secret.go b/secret-sync-controller/pkg/util/secretutil/secret.go new file mode 100644 index 000000000..8bb30f21e --- /dev/null +++ b/secret-sync-controller/pkg/util/secretutil/secret.go @@ -0,0 +1,234 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 secretutil + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "sort" + "strings" + + secretsyncv1alpha1 "sigs.k8s.io/secrets-store-csi-driver/secret-sync-controller/api/v1alpha1" + + "golang.org/x/crypto/pkcs12" + corev1 "k8s.io/api/core/v1" +) + +const ( + certType = "CERTIFICATE" + privateKeyType = "PRIVATE KEY" + privateKeyTypeRSA = "RSA PRIVATE KEY" + privateKeyTypeEC = "EC PRIVATE KEY" +) + +// GetCertPart returns the certificate or the private key part of the cert +func GetCertPart(data []byte, key string) ([]byte, error) { + if key == corev1.TLSPrivateKeyKey { + return getPrivateKey(data) + } + if key == corev1.TLSCertKey { + return getCert(data) + } + return nil, fmt.Errorf("key '%s' is not supported. Only 'tls.key' and 'tls.crt' are supported", key) +} + +// getCert returns the certificate part of a cert +func getCert(data []byte) ([]byte, error) { + var certs []byte + for { + pemBlock, rest := pem.Decode(data) + if pemBlock == nil { + break + } + if pemBlock.Type == certType { + block := pem.EncodeToMemory(pemBlock) + certs = append(certs, block...) + } + data = rest + } + + // if cert is nil, then it might be a pfx cert + if certs == nil { + pemBlocks, err := pkcs12.ToPEM(data, "") + if err != nil { + return nil, err + } + + // pem Blocks returns both the certificate and private key types + for _, block := range pemBlocks { + // get bytes for certificate + if block.Type == certType { + certs = append(certs, pem.EncodeToMemory(block)...) + } + } + } + + return certs, nil +} + +// getPrivateKey returns the private key part of a cert +func getPrivateKey(data []byte) ([]byte, error) { + var der, derKey, rest []byte + var pemBlock *pem.Block + privKeyType := privateKeyType + + for { + pemBlock, rest = pem.Decode(data) + if pemBlock == nil { + break + } + if pemBlock.Type != certType { + der = pemBlock.Bytes + } + data = rest + } + + // if both der is nil, then certificate might be in the pfx format + if der == nil { + pemBlocks, err := pkcs12.ToPEM(data, "") + if err != nil { + return nil, err + } + + // pem blocks returns both the certificate and private key types + for _, block := range pemBlocks { + // get bytes for private key + if block.Type == privateKeyType { + der = block.Bytes + } + } + } + + // parses an RSA private key in PKCS #1, ASN.1 DER form + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + privKeyType = privateKeyTypeRSA + derKey = x509.MarshalPKCS1PrivateKey(key) + } + // parses an unencrypted private key in PKCS #8, ASN.1 DER form + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey: + derKey = x509.MarshalPKCS1PrivateKey(key) + privKeyType = privateKeyTypeRSA + case *ecdsa.PrivateKey: + derKey, err = x509.MarshalECPrivateKey(key) + privKeyType = privateKeyTypeEC + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unknown private key type found while getting key. Only rsa and ecdsa are supported") + } + } + // parses an EC private key in SEC 1, ASN.1 DER form + if key, err := x509.ParseECPrivateKey(der); err == nil { + derKey, err = x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + privKeyType = privateKeyTypeEC + } + block := &pem.Block{ + Type: privKeyType, + Bytes: derKey, + } + + return pem.EncodeToMemory(block), nil +} + +// GetSecretType returns a k8s secret type. +// Kubernetes doesn't impose any constraints on the type name: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types +// If the secret type is empty, then default is Opaque. +func GetSecretType(sType string) corev1.SecretType { + if sType == "" { + return corev1.SecretTypeOpaque + } + return corev1.SecretType(sType) +} + +// ValidateSecretObject performs basic validation of the secret provider class +// secret object to check if the mandatory fields - name, type and data are defined +func ValidateSecretObject(secretName string, secretObj secretsyncv1alpha1.SecretObject) error { + if len(secretName) == 0 { + return fmt.Errorf("secret name is empty") + } + if len(secretObj.Type) == 0 { + return fmt.Errorf("secret type is empty") + } + if len(secretObj.Data) == 0 { + return fmt.Errorf("data is empty") + } + return nil +} + +// GetSecretData gets the object contents from the pods target path and returns a +// map that will be populated in the Kubernetes secret data field +func GetSecretData(secretObjData []secretsyncv1alpha1.SecretObjectData, secretType corev1.SecretType, files map[string][]byte) (map[string][]byte, error) { + datamap := make(map[string][]byte) + for _, data := range secretObjData { + sourcePath := strings.TrimSpace(data.SourcePath) + dataKey := strings.TrimSpace(data.TargetKey) + + if len(sourcePath) == 0 { + return datamap, fmt.Errorf("object name in secretObject.data") + } + if len(dataKey) == 0 { + return datamap, fmt.Errorf("key in secretObject.data is empty") + } + content, ok := files[sourcePath] + if !ok { + return datamap, fmt.Errorf("file matching sourcePath %s not found in the pod", sourcePath) + } + datamap[dataKey] = content + if secretType == corev1.SecretTypeTLS { + c, err := GetCertPart(content, dataKey) + if err != nil { + return datamap, fmt.Errorf("failed to get cert data for %s: %w", dataKey, err) + } + datamap[dataKey] = c + } + } + return datamap, nil +} + +// GetSHAFromSecret gets SHA for the secret data +func GetSHAFromSecret(data map[string][]byte) (string, error) { + values := make([]string, 0, len(data)) + for k, v := range data { + values = append(values, k+"="+string(v)) + } + // sort the values to always obtain a deterministic SHA for + // same content in different order + sort.Strings(values) + return generateSHA(strings.Join(values, ";")) +} + +// generateSHA generates SHA from string +func generateSHA(data string) (string, error) { + hasher := sha256.New() + _, err := io.WriteString(hasher, data) + if err != nil { + return "", err + } + sha := hasher.Sum(nil) + return fmt.Sprintf("%x", sha), nil +} diff --git a/test/bats/e2e-provider-ssc.bats b/test/bats/e2e-provider-ssc.bats new file mode 100644 index 000000000..9a38e9191 --- /dev/null +++ b/test/bats/e2e-provider-ssc.bats @@ -0,0 +1,70 @@ +#!/usr/bin/env bats + +load helpers + +BATS_TESTS_DIR=test/bats/tests/e2e_provider_ssc +WAIT_TIME=60 +SLEEP_TIME=1 + +@test "secretproviderclasses crd is established" { + kubectl wait --for condition=established --timeout=60s crd/secretproviderclasses.secrets-store.csi.x-k8s.io + + run kubectl get crd/secretproviderclasses.secrets-store.csi.x-k8s.io + assert_success +} + +@test "secretsync crd is established" { + kubectl wait --for condition=established --timeout=60s crd/secretsyncs.secret-sync.x-k8s.io + + run kubectl get crd/secretsyncs.secret-sync.x-k8s.io + assert_success +} + +@test "Test rbac roles and role bindings exist" { + run kubectl get clusterrole/secret-sync-controller-manager-role + assert_success + + run kubectl get clusterrolebinding/secret-sync-controller-manager-rolebinding + assert_success +} + +@test "[v1alpha1] deploy e2e-providerspc secretproviderclass crd" { + kubectl create namespace test-v1alpha1 --dry-run=client -o yaml | kubectl apply -f - + + envsubst < $BATS_TESTS_DIR/e2e_provider_v1_secretproviderclass.yaml | kubectl apply -n test-v1alpha1 -f - + + kubectl wait --for condition=established -n test-v1alpha1 --timeout=60s crd/secretproviderclasses.secrets-store.csi.x-k8s.io + + cmd="kubectl get secretproviderclasses.secrets-store.csi.x-k8s.io/e2e-providerspc -n test-v1alpha1 -o yaml | grep e2e-providerspc" + wait_for_process $WAIT_TIME $SLEEP_TIME "$cmd" +} + +@test "[v1alpha1] deploy e2e-providerspc secretsync crd" { + # Create the SPC + envsubst < $BATS_TESTS_DIR/e2e_provider_v1_secretproviderclass.yaml | kubectl apply -n test-v1alpha1 -f - + + kubectl wait --for condition=established -n test-v1alpha1 --timeout=60s crd/secretproviderclasses.secrets-store.csi.x-k8s.io + + cmd="kubectl get secretproviderclasses.secrets-store.csi.x-k8s.io/e2e-providerspc -n test-v1alpha1 -o yaml | grep e2e-providerspc" + wait_for_process $WAIT_TIME $SLEEP_TIME "$cmd" + + # Create the SecretSync + envsubst < $BATS_TESTS_DIR/e2e_provider_v1alpha1_secretsync.yaml | kubectl apply -n test-v1alpha1 -f - + + kubectl wait --for condition=established -n test-v1alpha1 --timeout=60s crd/secretsyncs.secret-sync.x-k8s.io + + cmd="kubectl get secretsyncs.secret-sync.x-k8s.io/sse2esecret -n test-v1alpha1 -o yaml | grep sse2esecret" + wait_for_process $WAIT_TIME $SLEEP_TIME "$cmd" + + # Retrieve the secret + cmd="kubectl get secret sse2esecret -n test-v1alpha1 -o yaml | grep 'apiVersion: secret-sync.x-k8s.io/v1alpha1'" + wait_for_process $WAIT_TIME $SLEEP_TIME "$cmd" +} + +teardown_file() { + if [[ "${INPLACE_UPGRADE_TEST}" != "true" ]]; then + #cleanup + run kubectl delete namespace test-ns + run kubectl delete namespace test-v1alpha1 + fi +} diff --git a/test/bats/tests/e2e_provider_ssc/e2e_provider_v1_secretproviderclass.yaml b/test/bats/tests/e2e_provider_ssc/e2e_provider_v1_secretproviderclass.yaml new file mode 100644 index 000000000..c9c4f4b3f --- /dev/null +++ b/test/bats/tests/e2e_provider_ssc/e2e_provider_v1_secretproviderclass.yaml @@ -0,0 +1,12 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: e2e-providerspc +spec: + provider: e2e-provider + parameters: + objects: | + array: + - | + objectName: foo + objectVersion: v1 diff --git a/test/bats/tests/e2e_provider_ssc/e2e_provider_v1alpha1_secretsync.yaml b/test/bats/tests/e2e_provider_ssc/e2e_provider_v1alpha1_secretsync.yaml new file mode 100644 index 000000000..e208f26f1 --- /dev/null +++ b/test/bats/tests/e2e_provider_ssc/e2e_provider_v1alpha1_secretsync.yaml @@ -0,0 +1,12 @@ +apiVersion: secret-sync.x-k8s.io/v1alpha1 +kind: SecretSync +metadata: + name: sse2esecret +spec: + serviceAccountName: default + secretProviderClassName: e2e-providerspc + secretObject: + type: Opaque + data: + - sourcePath: foo + targetKey: foo-key \ No newline at end of file