├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── agekey_types.go │ ├── agekey_webhook.go │ ├── agekey_webhook_test.go │ ├── agesecret_types.go │ ├── agesecret_webhook.go │ ├── agesecret_webhook_test.go │ ├── groupversion_info.go │ ├── webhook_suite_test.go │ └── zz_generated.deepcopy.go ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── gitopssecret.snappcloud.io_agekeys.yaml │ │ └── gitopssecret.snappcloud.io_agesecrets.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_agekeys.yaml │ │ ├── cainjection_in_agesecrets.yaml │ │ ├── webhook_in_agekeys.yaml │ │ └── webhook_in_agesecrets.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_config_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── agekey_editor_role.yaml │ ├── agekey_viewer_role.yaml │ ├── agesecret_editor_role.yaml │ ├── agesecret_viewer_role.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── custom_role.yaml │ ├── custom_role_binding.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── samples │ ├── _v1alpha1_agekey.yaml │ ├── _v1alpha1_agekey2.yaml │ ├── _v1alpha1_agekey3.yaml │ ├── _v1alpha1_agekey_invalid.yaml │ └── _v1alpha1_agesecret.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── consts └── consts.go ├── controllers ├── agekey_controller.go ├── agekey_controller_test.go ├── agesecret_controller.go ├── agesecret_controller_test.go └── suite_test.go ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── k8sutils ├── agekey.go ├── agesecret.go └── finalizer.go ├── lang └── lang.go └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | assignees: 8 | - navid.shariaty 9 | labels: 10 | - dependencies 11 | groups: 12 | dev-dependencies: 13 | patterns: 14 | - "*" 15 | 16 | - package-ecosystem: "docker" 17 | directory: "/" 18 | schedule: 19 | interval: "monthly" 20 | assignees: 21 | - navid.shariaty 22 | labels: 23 | - dependencies 24 | groups: 25 | dev-dependencies: 26 | patterns: 27 | - "*" 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ v* ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | lint: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: golangci-lint 17 | uses: golangci/golangci-lint-action@v3.7.0 18 | with: 19 | version: latest 20 | args: --timeout 5m 21 | test-api: 22 | name: test-api 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: run api tests using make 27 | run: make test-api 28 | test-controller: 29 | name: test-controller 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: run controller tests using make 34 | run: make test-controller 35 | docker: 36 | name: docker 37 | runs-on: ubuntu-latest 38 | needs: 39 | - lint 40 | - test-api 41 | - test-controller 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: docker/setup-qemu-action@v3 45 | - uses: docker/setup-buildx-action@v3 46 | - uses: docker/login-action@v3 47 | with: 48 | registry: ghcr.io 49 | username: ${{ github.repository_owner }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | - uses: docker/metadata-action@v5 52 | id: meta 53 | with: 54 | images: ghcr.io/${{ github.repository }} 55 | tags: | 56 | type=ref,event=branch 57 | type=ref,event=pr 58 | type=semver,pattern={{version}} 59 | type=semver,pattern={{major}}.{{minor}} 60 | - uses: docker/build-push-action@v5 61 | with: 62 | file: "Dockerfile" 63 | context: . 64 | platforms: linux/amd64 65 | push: true 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | .vscode/ 28 | .DS_Store/ 29 | keys/ 30 | bin/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | COPY k8sutils/ k8sutils/ 17 | COPY consts/ consts/ 18 | COPY lang/ lang/ 19 | 20 | # Build 21 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 22 | 23 | # Use distroless as minimal base image to package the manager binary 24 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 25 | FROM alpine:3.20 26 | WORKDIR / 27 | COPY --from=builder /workspace/manager . 28 | USER 65532:65532 29 | 30 | ENTRYPOINT ["/manager"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= controller:latest 4 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 5 | ENVTEST_K8S_VERSION = 1.24.2 6 | 7 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | 14 | # Setting SHELL to bash allows bash commands to be executed by recipes. 15 | # This is a requirement for 'setup-envtest.sh' in the test target. 16 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 17 | SHELL = /usr/bin/env bash -o pipefail 18 | .SHELLFLAGS = -ec 19 | 20 | .PHONY: all 21 | all: build 22 | 23 | ##@ General 24 | 25 | # The help target prints out all targets with their descriptions organized 26 | # beneath their categories. The categories are represented by '##@' and the 27 | # target descriptions by '##'. The awk commands is responsible for reading the 28 | # entire set of makefiles included in this invocation, looking for lines of the 29 | # file as xyz: ## something, and then pretty-format the target and help. Then, 30 | # if there's a line with ##@ something, that gets pretty-printed as a category. 31 | # More info on the usage of ANSI control characters for terminal formatting: 32 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 33 | # More info on the awk command: 34 | # http://linuxcommand.org/lc3_adv_awk.php 35 | 36 | .PHONY: help 37 | help: ## Display this help. 38 | @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) 39 | 40 | ##@ Development 41 | 42 | .PHONY: manifests 43 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 44 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 45 | 46 | .PHONY: generate 47 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 48 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 49 | 50 | .PHONY: fmt 51 | fmt: ## Run go fmt against code. 52 | go fmt ./... 53 | 54 | .PHONY: vet 55 | vet: ## Run go vet against code. 56 | go vet ./... 57 | 58 | .PHONY: test 59 | test: manifests generate fmt vet envtest ## Run tests. 60 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out 61 | 62 | .PHONY: test-controller 63 | test-controller: manifests generate fmt vet envtest ## Run tests. 64 | ACK_GINKGO_DEPRECATIONS=1.16.5 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v ./controllers/... 65 | 66 | .PHONY: test-api 67 | test-api: manifests generate fmt vet envtest ## Run tests. 68 | ACK_GINKGO_DEPRECATIONS=1.16.5 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v ./api/... 69 | 70 | ##@ Build 71 | 72 | .PHONY: build 73 | build: generate fmt vet ## Build manager binary. 74 | go build -o bin/manager main.go 75 | 76 | .PHONY: run 77 | run: manifests generate fmt vet ## Run a controller from your host. 78 | go run ./main.go 79 | 80 | .PHONY: docker-build 81 | docker-build: test ## Build docker image with the manager. 82 | docker build -t ${IMG} . 83 | 84 | .PHONY: docker-push 85 | docker-push: ## Push docker image with the manager. 86 | docker push ${IMG} 87 | 88 | ##@ Deployment 89 | 90 | ifndef ignore-not-found 91 | ignore-not-found = false 92 | endif 93 | 94 | .PHONY: install 95 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 96 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 97 | 98 | .PHONY: uninstall 99 | 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. 100 | $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 101 | 102 | .PHONY: deploy 103 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 104 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 105 | $(KUSTOMIZE) build config/default | kubectl apply -f - 106 | 107 | .PHONY: undeploy 108 | 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. 109 | $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 110 | 111 | ##@ Build Dependencies 112 | 113 | ## Location to install dependencies to 114 | LOCALBIN ?= $(shell pwd)/bin 115 | $(LOCALBIN): 116 | mkdir -p $(LOCALBIN) 117 | 118 | ## Tool Binaries 119 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 120 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 121 | ENVTEST ?= $(LOCALBIN)/setup-envtest 122 | 123 | ## Tool Versions 124 | KUSTOMIZE_VERSION ?= v4.5.5 125 | CONTROLLER_TOOLS_VERSION ?= v0.12.0 126 | 127 | KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" 128 | .PHONY: kustomize 129 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 130 | $(KUSTOMIZE): $(LOCALBIN) 131 | curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) 132 | 133 | .PHONY: controller-gen 134 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 135 | $(CONTROLLER_GEN): $(LOCALBIN) 136 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 137 | 138 | .PHONY: envtest 139 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 140 | $(ENVTEST): $(LOCALBIN) 141 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 142 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: gitopssecret.snappcloud.io 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: gitops-secret-manager 5 | repo: github.com/snapp-incubator/age-operator 6 | resources: 7 | - api: 8 | crdVersion: v1 9 | namespaced: true 10 | controller: true 11 | domain: gitopssecret.snappcloud.io 12 | kind: AgeKey 13 | path: github.com/snapp-incubator/age-operator/api/v1alpha1 14 | version: v1alpha1 15 | webhooks: 16 | defaulting: true 17 | validation: true 18 | webhookVersion: v1 19 | - api: 20 | crdVersion: v1 21 | namespaced: true 22 | controller: true 23 | domain: gitopssecret.snappcloud.io 24 | kind: AgeSecret 25 | path: github.com/snapp-incubator/age-operator/api/v1alpha1 26 | version: v1alpha1 27 | webhooks: 28 | defaulting: true 29 | validation: true 30 | webhookVersion: v1 31 | version: "3" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Age-Operator 2 | This repository is a secret manager for kubernetes using [age](https://github.com/FiloSottile/age). 3 | 4 | ### What is secret manager? 5 | By default, you put all your configuration on a codebase. There may be sensitive data like passwords or tokens among them. a secret manager lets you encrypt your sensitive data and then push it to codebase. 6 | 7 | ## Description 8 | Instead of pushing raw data to git, encrypt your data, push it to codebase, apply it to kubernetes using gitops, and kubernetes will use this operator to create a [secret](https://kubernetes.io/docs/concepts/configuration/secret/) for you. then you can use that secret for your deployments. 9 | 10 | ## Getting Started 11 | 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. 12 | **Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). 13 | 14 | ## Step-by-Step guide through using age-operator 15 | 16 | ### Age CLI Installation 17 | 18 | You can use [this link](https://github.com/FiloSottile/age#installation) to install `age`. Also, you can build from source if you have a supported version of golang installed on your system. It's mentioned in the given link. 19 | 20 | ### Generate Age Keys 21 | 22 | You need to generate `age keys` by running the code below simply: 23 | 24 | ```sh 25 | age-keygen -o {key file name} 26 | # For example 27 | age-keygen -o key.txt 28 | ``` 29 | 30 | If you look inside the file, it has three lines. 31 | 32 | ```text 33 | # created: 2022-xx-yyT00:00:00+04:30 34 | # public key: age1fjn89y8svr9rdqh6c9h6drxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 35 | AGE-SECRET-KEY-1ZQ3729NNAAP8MAFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 36 | ``` 37 | 38 | - The first line shows the creation timestamp of the Age key. 39 | - The second line is the public key. It's used to encrypt data. 40 | - The third line is the secret key. It's used to create an AgeKey object that decrypts the AgeSecret object. 41 | 42 | ### Encryption with Age 43 | 44 | For encryption, you will need a `public key` and a file that contains your sensitive data. your yaml data should be flat. nested yaml files are not supported now. 45 | 46 | sample data.yaml: 47 | 48 | ```yaml 49 | password: my_password 50 | token: my_token 51 | ``` 52 | 53 | and then run: 54 | 55 | ```sh 56 | age -r {public key} -e -a {plaintext data file} > {encrypted data file} 57 | # for example 58 | age -r age1fjn89y8svr9rdqh6c9h6drxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -e -a data.yaml > data.age 59 | ``` 60 | 61 | If you look at the `data.age` file, it contains data like this (surely content differs). 62 | 63 | ```text 64 | -----BEGIN AGE ENCRYPTED FILE----- 65 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXbXlPYUxHRTVMRGxBdkxr 66 | Zk5VZXpSR0Npc2EzYmRUbFNUbTVRRUpvb0ZjCjk1blA2QTFmWHN5akV1aDhGQUJR 67 | RTBwWmRvaFJjUWlYcFBBdS93bFBiaGMKLS0tIE5LZDN4aElNMEhwZXcwWW9ZUmdN 68 | bTI0V0NGWTJkTElaRmFQNjhWREQ3bXcKKHYCUSb/xvPlj5umQRFwwd1ULlXDTYXw 69 | jFRZvb9z4cXANc6Vp6kK8aoXNw0EzT46WId4KtTgCVwl7UDcgj+LXiO/e4J/2Rk0 70 | 0z1P3YUb 71 | -----END AGE ENCRYPTED FILE----- 72 | ``` 73 | 74 | ### Decryption with Age 75 | 76 | To decrypt data, you will need a `secret key` and an encrypted file. 77 | 78 | ```sh 79 | age -d -i {secret key file} {encrypted data file} 80 | # For example 81 | age -d -i key.txt data.age 82 | ``` 83 | 84 | ## Kubernetes Resources 85 | 86 | ### AgeKey object 87 | 88 | To create an `AgeKey` object, you need to create a template similar to the following. 89 | 90 | ```yaml 91 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 92 | kind: AgeKey 93 | metadata: 94 | name: {fill name} 95 | namespace: {fill namespace} 96 | spec: 97 | ageSecretKey: {fill with secret key} 98 | ``` 99 | 100 | Here is an `AgeKey` sample: 101 | 102 | ```bash 103 | oc get AgeKey agekey-sample -o yaml 104 | ``` 105 | 106 | ```yaml 107 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 108 | kind: AgeKey 109 | metadata: 110 | name: agekey-sample 111 | namespace: default 112 | spec: 113 | ageSecretKey: "AGE-SECRET-KEY-1ZQ3729NNAAP8MAFXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 114 | ``` 115 | 116 | ### AgeSecret object 117 | 118 | You can create an `AgeSecret` object with the below template. You can find more information about each field in the [AgeSecret Fields](./secret-management.md#agesecret) section: 119 | 120 | ```yaml 121 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 122 | kind: AgeSecret 123 | metadata: 124 | name: {fill name} 125 | namespace: {fill namespace} 126 | labels: 127 | ... {fill labels} 128 | annotations: 129 | ... {fill annotations} 130 | spec: 131 | labelsToRemove: 132 | - {label1} 133 | - {label2} 134 | suspend: {fill suspend} 135 | ageKeyRef: {fill ref name} 136 | stringData: 137 | ... {fill stringData} 138 | ``` 139 | 140 | #### AgeSecret 141 | 142 | -
name
Name of the AgeSecret object. If the decryption process was successful, the controller will generate a Secret object with this name. 143 | 144 | -
namespace
145 | The namespace of the AgeSecret object, the secret will get created in this namespace. 146 | 147 | -
labels
148 | A set of "key:value" that will be copied inside the generated secret. 149 | 150 | -
annotations
151 | A set of "key:value" that will be copied inside the generated secret. 152 | 153 | -
labelsToRemove
154 | An array of labels to remove while creating the child secret, and not to inherit them. Sample use-case is inside CD on k8s, when you want a label selector to track "AgeSecret" but not the child secret. 155 | 156 | -
suspend
157 | It's boolean. The default value is false. It determines whether the controller should reconcile on changes and apply changes to secret or you are just testing and the controller should not change anything. 158 | 159 | -
ageKeyRef
160 | This field indicates the name of the AgeKey object that you have encrypted this AgeSecret with it, so it has to be the same as the "AgeKey metadata.name" field's value. 161 | 162 | -
stringData
163 | This field is where you should put the encrypted Age message without its "-----BEGIN AGE ENCRYPTED FILE-----" prefix and "-----END AGE ENCRYPTED FILE-----" suffix. 164 | 165 | > [!NOTE] 166 | > Be careful not to push files containing "secret key" or "plain configuration" on git during the encryption and decryption steps. 167 | 168 | Here is an `AgeSecret` sample: 169 | 170 | ```bash 171 | oc get AgeSecret agesecret-sample -o yaml 172 | ``` 173 | 174 | ```yaml 175 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 176 | kind: AgeSecret 177 | metadata: 178 | name: agesecret-sample 179 | namespace: test-age-secret 180 | labels: 181 | key_label: value_label 182 | annotations: 183 | key_annotation: value_annotation 184 | spec: 185 | suspend: false 186 | ageKeyRef: agekey-sample 187 | stringData: | 188 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXbXlPYUxHRTVMRGxBdkxr 189 | Zk5VZXpSR0Npc2EzYmRUbFNUbTVRRUpvb0ZjCjk1blA2QTFmWHN5akV1aDhGQUJR 190 | RTBwWmRvaFJjUWlYcFBBdS93bFBiaGMKLS0tIE5LZDN4aElNMEhwZXcwWW9ZUmdN 191 | bTI0V0NGWTJkTElaRmFQNjhWREQ3bXcKKHYCUSb/xvPlj5umQRFwwd1ULlXDTYXw 192 | jFRZvb9z4cXANc6Vp6kK8aoXNw0EzT46WId4KtTgCVwl7UDcgj+LXiO/e4J/2Rk0 193 | 0z1P3YUb 194 | ``` 195 | 196 | ## Installation 197 | 198 | ### Running on the cluster 199 | 1. Install Instances of Custom Resources: 200 | 201 | ```sh 202 | kubectl apply -f config/samples/ 203 | ``` 204 | 205 | 2. Build and push your image to the location specified by `IMG`: 206 | 207 | ```sh 208 | make docker-build docker-push IMG=/gitops-secret-manager:tag 209 | ``` 210 | 211 | 3. Deploy the controller to the cluster with the image specified by `IMG`: 212 | 213 | ```sh 214 | make deploy IMG=/gitops-secret-manager:tag 215 | ``` 216 | 217 | ### Uninstall CRDs 218 | To delete the CRDs from the cluster: 219 | 220 | ```sh 221 | make uninstall 222 | ``` 223 | 224 | ### Undeploy controller 225 | UnDeploy the controller to the cluster: 226 | 227 | ```sh 228 | make undeploy 229 | ``` 230 | 231 | ## Contributing 232 | After forking this repository, add your code and add tests for your code. then make sure that test cases are alright. 233 | 234 | ```sh 235 | make test 236 | 237 | # to test api with verbosity 238 | make test-api 239 | 240 | # to test controller with verbosity 241 | make test-controller 242 | ``` 243 | 244 | ### How it works 245 | This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 246 | 247 | It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) 248 | which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster 249 | 250 | ### Test It Out 251 | 1. Install the CRDs into the cluster: 252 | 253 | ```sh 254 | make install 255 | ``` 256 | 257 | 2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): 258 | 259 | ```sh 260 | make run 261 | ``` 262 | 263 | And in case you don't want to enable webhook in local, run command below and comment [WEBHOOK] sections in config/crd. 264 | 265 | ```sh 266 | make run ENABLE_WEBHOOKS=false 267 | ``` 268 | 269 | **NOTE:** You can also run this in one step by running: `make install run` 270 | 271 | ### Modifying the API definitions 272 | If you are editing the API definitions, generate the manifests such as CRs or CRDs using: 273 | 274 | ```sh 275 | make manifests 276 | ``` 277 | 278 | **NOTE:** Run `make --help` for more information on all potential `make` targets 279 | 280 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 281 | 282 | ## License 283 | 284 | Copyright 2022. 285 | 286 | Licensed under the Apache License, Version 2.0 (the "License"); 287 | you may not use this file except in compliance with the License. 288 | You may obtain a copy of the License at 289 | 290 | http://www.apache.org/licenses/LICENSE-2.0 291 | 292 | Unless required by applicable law or agreed to in writing, software 293 | distributed under the License is distributed on an "AS IS" BASIS, 294 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 295 | See the License for the specific language governing permissions and 296 | limitations under the License. 297 | 298 | -------------------------------------------------------------------------------- /api/v1alpha1/agekey_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // AgeKeySpec defines the desired state of AgeKey 24 | type AgeKeySpec struct { 25 | // +kubebuilder:validation:Required 26 | AgeSecretKey string `json:"ageSecretKey"` 27 | } 28 | 29 | // AgeKeyStatus defines the observed state of AgeKey 30 | type AgeKeyStatus struct { 31 | Message string `json:"message"` 32 | } 33 | 34 | //+kubebuilder:object:root=true 35 | //+kubebuilder:subresource:status 36 | 37 | // AgeKey is the Schema for the agekeys API 38 | // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.message` 39 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 40 | type AgeKey struct { 41 | metav1.TypeMeta `json:",inline"` 42 | metav1.ObjectMeta `json:"metadata,omitempty"` 43 | 44 | Spec AgeKeySpec `json:"spec,omitempty"` 45 | Status AgeKeyStatus `json:"status,omitempty"` 46 | } 47 | 48 | //+kubebuilder:object:root=true 49 | 50 | // AgeKeyList contains a list of AgeKey 51 | type AgeKeyList struct { 52 | metav1.TypeMeta `json:",inline"` 53 | metav1.ListMeta `json:"metadata,omitempty"` 54 | Items []AgeKey `json:"items"` 55 | } 56 | 57 | func init() { 58 | SchemeBuilder.Register(&AgeKey{}, &AgeKeyList{}) 59 | } 60 | -------------------------------------------------------------------------------- /api/v1alpha1/agekey_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "filippo.io/age" 21 | "fmt" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 27 | "strings" 28 | ) 29 | 30 | // log is for logging in this package. 31 | var ( 32 | ageKeyLog = logf.Log.WithName("agekey-resource") 33 | ageKeyPrefix = "AGE-SECRET-KEY-" 34 | ) 35 | 36 | func (r *AgeKey) SetupWebhookWithManager(mgr ctrl.Manager) error { 37 | return ctrl.NewWebhookManagedBy(mgr). 38 | For(r). 39 | Complete() 40 | } 41 | 42 | //+kubebuilder:webhook:path=/mutate-gitopssecret-snappcloud-io-v1alpha1-agekey,mutating=true,failurePolicy=fail,sideEffects=None,groups=gitopssecret.snappcloud.io,resources=agekeys,verbs=create;update,versions=v1alpha1,name=magekey.kb.io,admissionReviewVersions=v1 43 | 44 | var _ webhook.Defaulter = &AgeKey{} 45 | 46 | // Default implements webhook.Defaulter so a webhook will be registered for the type 47 | func (r *AgeKey) Default() { 48 | ageKeyLog.Info("default", "name", r.Name) 49 | } 50 | 51 | //+kubebuilder:webhook:path=/validate-gitopssecret-snappcloud-io-v1alpha1-agekey,mutating=false,failurePolicy=fail,sideEffects=None,groups=gitopssecret.snappcloud.io,resources=agekeys,verbs=create;update,versions=v1alpha1,name=vagekey.kb.io,admissionReviewVersions=v1 52 | 53 | var _ webhook.Validator = &AgeKey{} 54 | 55 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 56 | func (r *AgeKey) ValidateCreate() (admission.Warnings, error) { 57 | ageKeyLog.Info("validate create", "name", r.Name) 58 | if err := r.ValidateAgeKey(); err != nil { 59 | return nil, err 60 | } 61 | return nil, nil 62 | } 63 | 64 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 65 | func (r *AgeKey) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 66 | ageKeyLog.Info("validate update", "name", r.Name) 67 | if err := r.ValidateAgeKey(); err != nil { 68 | return nil, err 69 | } 70 | return nil, nil 71 | } 72 | 73 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 74 | func (r *AgeKey) ValidateDelete() (admission.Warnings, error) { 75 | ageKeyLog.Info("validate delete", "name", r.Name) 76 | 77 | return nil, nil 78 | } 79 | 80 | func (r *AgeKey) ValidateAgeKey() error { 81 | if !strings.HasPrefix(r.Spec.AgeSecretKey, ageKeyPrefix) { 82 | return fmt.Errorf("AgeKey must start with %v prefix", ageKeyPrefix) 83 | } 84 | if _, err := age.ParseX25519Identity(r.Spec.AgeSecretKey); err != nil { 85 | ageKeyLog.Info("validate AgeKey", "name", r.Name, "error", err) 86 | return fmt.Errorf("provided AgeKey is not valid") 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /api/v1alpha1/agekey_webhook_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | "k8s.io/apimachinery/pkg/api/errors" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | var _ = Describe("", func() { 11 | const ( 12 | fooName = "foo-agekey" 13 | fooNamespace = "default" 14 | ) 15 | var ( 16 | err error 17 | ) 18 | fooAgeKeyTypeMeta := metav1.TypeMeta{ 19 | APIVersion: "gitopssecret.snappcloud.io/v1alpha1", 20 | Kind: "AgeKey", 21 | } 22 | fooAgeKeyObjectMeta := metav1.ObjectMeta{ 23 | Name: fooName, 24 | Namespace: fooNamespace, 25 | } 26 | fooAgeKeyMeta := &AgeKey{ 27 | TypeMeta: fooAgeKeyTypeMeta, 28 | ObjectMeta: fooAgeKeyObjectMeta, 29 | } 30 | 31 | AfterEach(func() { 32 | err = k8sClient.Delete(ctx, fooAgeKeyMeta) 33 | if err != nil { 34 | Expect(errors.IsNotFound(err)).Should(BeTrue()) 35 | } 36 | }) 37 | 38 | Context("When creating AgeKey", func() { 39 | It("should fail if AgeKey is empty", func() { 40 | fooAgeKey := &AgeKey{ 41 | TypeMeta: fooAgeKeyMeta.TypeMeta, 42 | ObjectMeta: fooAgeKeyMeta.ObjectMeta, 43 | Spec: AgeKeySpec{ 44 | AgeSecretKey: "", 45 | }, 46 | } 47 | err = k8sClient.Create(ctx, fooAgeKey) 48 | Expect(err).NotTo(BeNil()) 49 | }) 50 | 51 | It("should fail if AgeKey only has required prefix", func() { 52 | fooAgeKey := &AgeKey{ 53 | TypeMeta: fooAgeKeyMeta.TypeMeta, 54 | ObjectMeta: fooAgeKeyMeta.ObjectMeta, 55 | Spec: AgeKeySpec{ 56 | AgeSecretKey: ageKeyPrefix, 57 | }, 58 | } 59 | err = k8sClient.Create(ctx, fooAgeKey) 60 | Expect(err).NotTo(BeNil()) 61 | }) 62 | 63 | It("should pass if AgeKey is valid", func() { 64 | fooAgeKey := &AgeKey{ 65 | TypeMeta: fooAgeKeyMeta.TypeMeta, 66 | ObjectMeta: fooAgeKeyMeta.ObjectMeta, 67 | Spec: AgeKeySpec{ 68 | AgeSecretKey: "AGE-SECRET-KEY-1MDTY43V4JSAQF8LRVQ6698JKQJ56XH9JTUXJR7GRQ7CPFVAZQ5GQ5LKNLG", 69 | }, 70 | } 71 | err = k8sClient.Create(ctx, fooAgeKey) 72 | Expect(err).To(BeNil()) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /api/v1alpha1/agesecret_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // AgeSecretSpec defines the desired state of AgeSecret 24 | type AgeSecretSpec struct { 25 | AgeKeyRef string `json:"ageKeyRef"` 26 | StringData string `json:"stringData"` 27 | Suspend bool `json:"suspend,omitempty"` 28 | LabelsToRemove []string `json:"labelsToRemove,omitempty"` 29 | } 30 | 31 | // AgeSecretStatus defines the observed state of AgeSecret 32 | type AgeSecretStatus struct { 33 | Health string `json:"health"` 34 | Message string `json:"message"` 35 | } 36 | 37 | //+kubebuilder:object:root=true 38 | //+kubebuilder:subresource:status 39 | //+kubebuilder:printcolumn:name="Health",type=string,JSONPath=`.status.health` 40 | //+kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.message` 41 | //+kubebuilder:printcolumn:name="Suspended",type=string,JSONPath=`.spec.suspend` 42 | //+kubebuilder:printcolumn:name="AgeKey",type=string,JSONPath=`.spec.ageKeyRef` 43 | //+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 44 | 45 | // AgeSecret is the Schema for the AgeSecrets API 46 | type AgeSecret struct { 47 | metav1.TypeMeta `json:",inline"` 48 | metav1.ObjectMeta `json:"metadata,omitempty"` 49 | 50 | Spec AgeSecretSpec `json:"spec,omitempty"` 51 | Status AgeSecretStatus `json:"status,omitempty"` 52 | } 53 | 54 | //+kubebuilder:object:root=true 55 | 56 | // AgeSecretList contains a list of AgeSecret 57 | type AgeSecretList struct { 58 | metav1.TypeMeta `json:",inline"` 59 | metav1.ListMeta `json:"metadata,omitempty"` 60 | Items []AgeSecret `json:"items"` 61 | } 62 | 63 | func init() { 64 | SchemeBuilder.Register(&AgeSecret{}, &AgeSecretList{}) 65 | } 66 | -------------------------------------------------------------------------------- /api/v1alpha1/agesecret_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | logf "sigs.k8s.io/controller-runtime/pkg/log" 24 | "sigs.k8s.io/controller-runtime/pkg/webhook" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 26 | "strings" 27 | ) 28 | 29 | // log is for logging in this package. 30 | var ageSecretLog = logf.Log.WithName("agesecret-resource") 31 | 32 | func (r *AgeSecret) SetupWebhookWithManager(mgr ctrl.Manager) error { 33 | return ctrl.NewWebhookManagedBy(mgr). 34 | For(r). 35 | Complete() 36 | } 37 | 38 | //+kubebuilder:webhook:path=/mutate-gitopssecret-snappcloud-io-v1alpha1-agesecret,mutating=true,failurePolicy=fail,sideEffects=None,groups=gitopssecret.snappcloud.io,resources=agesecrets,verbs=create;update,versions=v1alpha1,name=magesecret.kb.io,admissionReviewVersions=v1 39 | 40 | var _ webhook.Defaulter = &AgeSecret{} 41 | 42 | // Default implements webhook.Defaulter so a webhook will be registered for the type 43 | func (r *AgeSecret) Default() { 44 | ageSecretLog.Info("default", "name", r.Name) 45 | 46 | } 47 | 48 | //+kubebuilder:webhook:path=/validate-gitopssecret-snappcloud-io-v1alpha1-agesecret,mutating=false,failurePolicy=fail,sideEffects=None,groups=gitopssecret.snappcloud.io,resources=agesecrets,verbs=create;update,versions=v1alpha1,name=vagesecret.kb.io,admissionReviewVersions=v1 49 | 50 | var _ webhook.Validator = &AgeSecret{} 51 | 52 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 53 | func (r *AgeSecret) ValidateCreate() (admission.Warnings, error) { 54 | ageSecretLog.Info("validate create", "name", r.Name) 55 | if err := r.ValidateAgeSecret(); err != nil { 56 | return nil, err 57 | } 58 | return nil, nil 59 | } 60 | 61 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 62 | func (r *AgeSecret) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 63 | ageSecretLog.Info("validate update", "name", r.Name) 64 | if err := r.ValidateAgeSecret(); err != nil { 65 | return nil, err 66 | } 67 | return nil, nil 68 | } 69 | 70 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 71 | func (r *AgeSecret) ValidateDelete() (admission.Warnings, error) { 72 | ageSecretLog.Info("validate delete", "name", r.Name) 73 | 74 | return nil, nil 75 | } 76 | 77 | func (r *AgeSecret) ValidateAgeSecret() error { 78 | if strings.TrimSpace(r.Spec.StringData) == "" { 79 | return fmt.Errorf("stringData can not be empty") 80 | } 81 | 82 | if strings.TrimSpace(r.Spec.AgeKeyRef) == "" { 83 | return fmt.Errorf("AgeKey reference can not be empty") 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /api/v1alpha1/agesecret_webhook_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | "k8s.io/apimachinery/pkg/api/errors" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | ) 10 | 11 | var _ = Describe("", func() { 12 | const ( 13 | fooName = "foo-agesecret" 14 | fooNamespace = "default" 15 | sampleStringData = "fake data" 16 | sampleAgeKey = "sample-agekey" 17 | ) 18 | var ( 19 | err error 20 | ) 21 | fooAgeSecretTypeMeta := metav1.TypeMeta{ 22 | APIVersion: "gitopssecret.snappcloud.io/v1alpha1", 23 | Kind: "AgeSecret", 24 | } 25 | fooAgeSecretObjectMeta := metav1.ObjectMeta{ 26 | Name: fooName, 27 | Namespace: fooNamespace, 28 | } 29 | fooAgeSecretMeta := &AgeSecret{ 30 | TypeMeta: fooAgeSecretTypeMeta, 31 | ObjectMeta: fooAgeSecretObjectMeta, 32 | } 33 | 34 | AfterEach(func() { 35 | err = k8sClient.Delete(ctx, fooAgeSecretMeta) 36 | if err != nil { 37 | Expect(errors.IsNotFound(err)).Should(BeTrue()) 38 | } 39 | }) 40 | 41 | Context("When creating AgeSecret", func() { 42 | It("should fail if ageKeyRef is empty", func() { 43 | fooAgeSecret := &AgeSecret{ 44 | TypeMeta: fooAgeSecretMeta.TypeMeta, 45 | ObjectMeta: fooAgeSecretMeta.ObjectMeta, 46 | Spec: AgeSecretSpec{ 47 | AgeKeyRef: "", 48 | StringData: sampleStringData, 49 | }, 50 | } 51 | err = k8sClient.Create(ctx, fooAgeSecret) 52 | Expect(err).NotTo(BeNil()) 53 | }) 54 | 55 | It("should fail if stringData is empty", func() { 56 | fooAgeSecret := &AgeSecret{ 57 | TypeMeta: fooAgeSecretMeta.TypeMeta, 58 | ObjectMeta: fooAgeSecretMeta.ObjectMeta, 59 | Spec: AgeSecretSpec{ 60 | AgeKeyRef: sampleAgeKey, 61 | StringData: "", 62 | }, 63 | } 64 | err = k8sClient.Create(ctx, fooAgeSecret) 65 | Expect(err).NotTo(BeNil()) 66 | }) 67 | 68 | It("should pass if AgeSecret is valid", func() { 69 | fooAgeSecret := &AgeSecret{ 70 | TypeMeta: fooAgeSecretMeta.TypeMeta, 71 | ObjectMeta: fooAgeSecretMeta.ObjectMeta, 72 | Spec: AgeSecretSpec{ 73 | AgeKeyRef: sampleAgeKey, 74 | StringData: sampleStringData, 75 | }, 76 | } 77 | err = k8sClient.Create(ctx, fooAgeSecret) 78 | Expect(err).To(BeNil()) 79 | 80 | fooAgeSecret2 := &AgeSecret{} 81 | err = k8sClient.Get(ctx, types.NamespacedName{Namespace: fooAgeSecret.GetNamespace(), Name: fooAgeSecret.GetName()}, fooAgeSecret2) 82 | Expect(err).To(BeNil()) 83 | Expect(fooAgeSecret2.Spec.Suspend).To(BeFalse()) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=gitopssecret.snappcloud.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "gitopssecret.snappcloud.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/webhook_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "context" 21 | "crypto/tls" 22 | "fmt" 23 | "net" 24 | "path/filepath" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | "testing" 27 | "time" 28 | 29 | . "github.com/onsi/ginkgo/v2" 30 | . "github.com/onsi/gomega" 31 | 32 | admissionv1beta1 "k8s.io/api/admission/v1beta1" 33 | //+kubebuilder:scaffold:imports 34 | "k8s.io/apimachinery/pkg/runtime" 35 | "k8s.io/client-go/rest" 36 | ctrl "sigs.k8s.io/controller-runtime" 37 | "sigs.k8s.io/controller-runtime/pkg/client" 38 | "sigs.k8s.io/controller-runtime/pkg/envtest" 39 | logf "sigs.k8s.io/controller-runtime/pkg/log" 40 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 41 | ) 42 | 43 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 44 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 45 | 46 | var cfg *rest.Config 47 | var k8sClient client.Client 48 | var testEnv *envtest.Environment 49 | var ctx context.Context 50 | var cancel context.CancelFunc 51 | 52 | func TestAPIs(t *testing.T) { 53 | RegisterFailHandler(Fail) 54 | RunSpecs(t, "Webhook Suite") 55 | } 56 | 57 | var _ = BeforeSuite(func() { 58 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 59 | 60 | ctx, cancel = context.WithCancel(context.TODO()) 61 | 62 | By("bootstrapping test environment") 63 | testEnv = &envtest.Environment{ 64 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 65 | ErrorIfCRDPathMissing: false, 66 | WebhookInstallOptions: envtest.WebhookInstallOptions{ 67 | Paths: []string{filepath.Join("..", "..", "config", "webhook")}, 68 | }, 69 | } 70 | 71 | var err error 72 | // cfg is defined in this file globally. 73 | cfg, err = testEnv.Start() 74 | Expect(err).NotTo(HaveOccurred()) 75 | Expect(cfg).NotTo(BeNil()) 76 | 77 | scheme := runtime.NewScheme() 78 | err = AddToScheme(scheme) 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | err = admissionv1beta1.AddToScheme(scheme) 82 | Expect(err).NotTo(HaveOccurred()) 83 | 84 | //+kubebuilder:scaffold:scheme 85 | 86 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 87 | Expect(err).NotTo(HaveOccurred()) 88 | Expect(k8sClient).NotTo(BeNil()) 89 | 90 | // start webhook server using Manager 91 | webhookInstallOptions := &testEnv.WebhookInstallOptions 92 | webhookSrv := webhook.NewServer(webhook.Options{ 93 | Host: webhookInstallOptions.LocalServingHost, 94 | Port: webhookInstallOptions.LocalServingPort, 95 | CertDir: webhookInstallOptions.LocalServingCertDir, 96 | }) 97 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 98 | Scheme: scheme, 99 | WebhookServer: webhookSrv, 100 | LeaderElection: false, 101 | }) 102 | Expect(err).NotTo(HaveOccurred()) 103 | 104 | err = (&AgeKey{}).SetupWebhookWithManager(mgr) 105 | Expect(err).NotTo(HaveOccurred()) 106 | 107 | err = (&AgeSecret{}).SetupWebhookWithManager(mgr) 108 | Expect(err).NotTo(HaveOccurred()) 109 | 110 | //+kubebuilder:scaffold:webhook 111 | 112 | go func() { 113 | defer GinkgoRecover() 114 | err = mgr.Start(ctx) 115 | Expect(err).NotTo(HaveOccurred()) 116 | }() 117 | 118 | // wait for the webhook server to get ready 119 | dialer := &net.Dialer{Timeout: time.Second} 120 | addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) 121 | Eventually(func() error { 122 | conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) 123 | if err != nil { 124 | return err 125 | } 126 | conn.Close() 127 | return nil 128 | }).Should(Succeed()) 129 | 130 | }) 131 | 132 | var _ = AfterSuite(func() { 133 | cancel() 134 | By("tearing down the test environment") 135 | err := testEnv.Stop() 136 | Expect(err).NotTo(HaveOccurred()) 137 | }) 138 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *AgeKey) DeepCopyInto(out *AgeKey) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | out.Spec = in.Spec 34 | out.Status = in.Status 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeKey. 38 | func (in *AgeKey) DeepCopy() *AgeKey { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(AgeKey) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *AgeKey) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *AgeKeyList) DeepCopyInto(out *AgeKeyList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]AgeKey, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeKeyList. 70 | func (in *AgeKeyList) DeepCopy() *AgeKeyList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(AgeKeyList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *AgeKeyList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *AgeKeySpec) DeepCopyInto(out *AgeKeySpec) { 89 | *out = *in 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeKeySpec. 93 | func (in *AgeKeySpec) DeepCopy() *AgeKeySpec { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(AgeKeySpec) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *AgeKeyStatus) DeepCopyInto(out *AgeKeyStatus) { 104 | *out = *in 105 | } 106 | 107 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeKeyStatus. 108 | func (in *AgeKeyStatus) DeepCopy() *AgeKeyStatus { 109 | if in == nil { 110 | return nil 111 | } 112 | out := new(AgeKeyStatus) 113 | in.DeepCopyInto(out) 114 | return out 115 | } 116 | 117 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 118 | func (in *AgeSecret) DeepCopyInto(out *AgeSecret) { 119 | *out = *in 120 | out.TypeMeta = in.TypeMeta 121 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 122 | in.Spec.DeepCopyInto(&out.Spec) 123 | out.Status = in.Status 124 | } 125 | 126 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeSecret. 127 | func (in *AgeSecret) DeepCopy() *AgeSecret { 128 | if in == nil { 129 | return nil 130 | } 131 | out := new(AgeSecret) 132 | in.DeepCopyInto(out) 133 | return out 134 | } 135 | 136 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 137 | func (in *AgeSecret) DeepCopyObject() runtime.Object { 138 | if c := in.DeepCopy(); c != nil { 139 | return c 140 | } 141 | return nil 142 | } 143 | 144 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 145 | func (in *AgeSecretList) DeepCopyInto(out *AgeSecretList) { 146 | *out = *in 147 | out.TypeMeta = in.TypeMeta 148 | in.ListMeta.DeepCopyInto(&out.ListMeta) 149 | if in.Items != nil { 150 | in, out := &in.Items, &out.Items 151 | *out = make([]AgeSecret, len(*in)) 152 | for i := range *in { 153 | (*in)[i].DeepCopyInto(&(*out)[i]) 154 | } 155 | } 156 | } 157 | 158 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeSecretList. 159 | func (in *AgeSecretList) DeepCopy() *AgeSecretList { 160 | if in == nil { 161 | return nil 162 | } 163 | out := new(AgeSecretList) 164 | in.DeepCopyInto(out) 165 | return out 166 | } 167 | 168 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 169 | func (in *AgeSecretList) DeepCopyObject() runtime.Object { 170 | if c := in.DeepCopy(); c != nil { 171 | return c 172 | } 173 | return nil 174 | } 175 | 176 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 177 | func (in *AgeSecretSpec) DeepCopyInto(out *AgeSecretSpec) { 178 | *out = *in 179 | if in.LabelsToRemove != nil { 180 | in, out := &in.LabelsToRemove, &out.LabelsToRemove 181 | *out = make([]string, len(*in)) 182 | copy(*out, *in) 183 | } 184 | } 185 | 186 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeSecretSpec. 187 | func (in *AgeSecretSpec) DeepCopy() *AgeSecretSpec { 188 | if in == nil { 189 | return nil 190 | } 191 | out := new(AgeSecretSpec) 192 | in.DeepCopyInto(out) 193 | return out 194 | } 195 | 196 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 197 | func (in *AgeSecretStatus) DeepCopyInto(out *AgeSecretStatus) { 198 | *out = *in 199 | } 200 | 201 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgeSecretStatus. 202 | func (in *AgeSecretStatus) DeepCopy() *AgeSecretStatus { 203 | if in == nil { 204 | return nil 205 | } 206 | out := new(AgeSecretStatus) 207 | in.DeepCopyInto(out) 208 | return out 209 | } 210 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | name: selfsigned-issuer 8 | namespace: system 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: system 17 | spec: 18 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/gitopssecret.snappcloud.io_agekeys.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.12.0 7 | name: agekeys.gitopssecret.snappcloud.io 8 | spec: 9 | group: gitopssecret.snappcloud.io 10 | names: 11 | kind: AgeKey 12 | listKind: AgeKeyList 13 | plural: agekeys 14 | singular: agekey 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .status.message 19 | name: Message 20 | type: string 21 | - jsonPath: .metadata.creationTimestamp 22 | name: Age 23 | type: date 24 | name: v1alpha1 25 | schema: 26 | openAPIV3Schema: 27 | description: AgeKey is the Schema for the agekeys API 28 | properties: 29 | apiVersion: 30 | description: 'APIVersion defines the versioned schema of this representation 31 | of an object. Servers should convert recognized schemas to the latest 32 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 33 | type: string 34 | kind: 35 | description: 'Kind is a string value representing the REST resource this 36 | object represents. Servers may infer this from the endpoint the client 37 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 38 | type: string 39 | metadata: 40 | type: object 41 | spec: 42 | description: AgeKeySpec defines the desired state of AgeKey 43 | properties: 44 | ageSecretKey: 45 | type: string 46 | required: 47 | - ageSecretKey 48 | type: object 49 | status: 50 | description: AgeKeyStatus defines the observed state of AgeKey 51 | properties: 52 | message: 53 | type: string 54 | required: 55 | - message 56 | type: object 57 | type: object 58 | served: true 59 | storage: true 60 | subresources: 61 | status: {} 62 | -------------------------------------------------------------------------------- /config/crd/bases/gitopssecret.snappcloud.io_agesecrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.12.0 7 | name: agesecrets.gitopssecret.snappcloud.io 8 | spec: 9 | group: gitopssecret.snappcloud.io 10 | names: 11 | kind: AgeSecret 12 | listKind: AgeSecretList 13 | plural: agesecrets 14 | singular: agesecret 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .status.health 19 | name: Health 20 | type: string 21 | - jsonPath: .status.message 22 | name: Message 23 | type: string 24 | - jsonPath: .spec.suspend 25 | name: Suspended 26 | type: string 27 | - jsonPath: .spec.ageKeyRef 28 | name: AgeKey 29 | type: string 30 | - jsonPath: .metadata.creationTimestamp 31 | name: Age 32 | type: date 33 | name: v1alpha1 34 | schema: 35 | openAPIV3Schema: 36 | description: AgeSecret is the Schema for the AgeSecrets API 37 | properties: 38 | apiVersion: 39 | description: 'APIVersion defines the versioned schema of this representation 40 | of an object. Servers should convert recognized schemas to the latest 41 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 42 | type: string 43 | kind: 44 | description: 'Kind is a string value representing the REST resource this 45 | object represents. Servers may infer this from the endpoint the client 46 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 47 | type: string 48 | metadata: 49 | type: object 50 | spec: 51 | description: AgeSecretSpec defines the desired state of AgeSecret 52 | properties: 53 | ageKeyRef: 54 | type: string 55 | labelsToRemove: 56 | items: 57 | type: string 58 | type: array 59 | stringData: 60 | type: string 61 | suspend: 62 | type: boolean 63 | required: 64 | - ageKeyRef 65 | - stringData 66 | type: object 67 | status: 68 | description: AgeSecretStatus defines the observed state of AgeSecret 69 | properties: 70 | health: 71 | type: string 72 | message: 73 | type: string 74 | required: 75 | - health 76 | - message 77 | type: object 78 | type: object 79 | served: true 80 | storage: true 81 | subresources: 82 | status: {} 83 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/gitopssecret.snappcloud.io_agekeys.yaml 6 | - bases/gitopssecret.snappcloud.io_agesecrets.yaml 7 | #+kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | - patches/webhook_in_agekeys.yaml 13 | - patches/webhook_in_agesecrets.yaml 14 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | - patches/cainjection_in_agekeys.yaml 19 | - patches/cainjection_in_agesecrets.yaml 20 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # the following config is for teaching kustomize how to do kustomization for CRDs. 23 | configurations: 24 | - kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_agekeys.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: agekeys.gitopssecret.snappcloud.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_agesecrets.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: agesecrets.gitopssecret.snappcloud.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_agekeys.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: agekeys.gitopssecret.snappcloud.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_agesecrets.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: agesecrets.gitopssecret.snappcloud.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: gitops-secret-manager-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: gitops-secret-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | - ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | - ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | #- manager_config_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | - manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | - webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | objref: 51 | kind: Certificate 52 | group: cert-manager.io 53 | version: v1 54 | name: serving-cert # this name should match the one in certificate.yaml 55 | fieldref: 56 | fieldpath: metadata.namespace 57 | - name: CERTIFICATE_NAME 58 | objref: 59 | kind: Certificate 60 | group: cert-manager.io 61 | version: v1 62 | name: serving-cert # this name should match the one in certificate.yaml 63 | - name: SERVICE_NAMESPACE # namespace of the service 64 | objref: 65 | kind: Service 66 | version: v1 67 | name: webhook-service 68 | fieldref: 69 | fieldpath: metadata.namespace 70 | - name: SERVICE_NAME 71 | objref: 72 | kind: Service 73 | version: v1 74 | name: webhook-service 75 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=0" 19 | ports: 20 | - containerPort: 8443 21 | protocol: TCP 22 | name: https 23 | resources: 24 | limits: 25 | cpu: 500m 26 | memory: 128Mi 27 | requests: 28 | cpu: 5m 29 | memory: 64Mi 30 | - name: manager 31 | args: 32 | - "--health-probe-bind-address=:8081" 33 | - "--metrics-bind-address=127.0.0.1:8080" 34 | - "--leader-elect" 35 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: 48ffd8e3.gitopssecret.snappcloud.io 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: controller 16 | newTag: latest 17 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | labels: 7 | control-plane: controller-manager 8 | spec: 9 | selector: 10 | matchLabels: 11 | control-plane: controller-manager 12 | replicas: 1 13 | template: 14 | metadata: 15 | annotations: 16 | kubectl.kubernetes.io/default-container: manager 17 | labels: 18 | control-plane: controller-manager 19 | spec: 20 | containers: 21 | - command: 22 | - /manager 23 | args: 24 | - --leader-elect 25 | image: ghcr.io/snapp-incubator/age-operator:main 26 | imagePullPolicy: Always 27 | name: manager 28 | livenessProbe: 29 | httpGet: 30 | path: /healthz 31 | port: 8081 32 | initialDelaySeconds: 15 33 | periodSeconds: 20 34 | readinessProbe: 35 | httpGet: 36 | path: /readyz 37 | port: 8081 38 | initialDelaySeconds: 5 39 | periodSeconds: 10 40 | resources: 41 | limits: 42 | cpu: 3 43 | memory: 2Gi 44 | requests: 45 | cpu: 1 46 | memory: 1Gi 47 | serviceAccountName: controller-manager 48 | terminationGracePeriodSeconds: 10 49 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/agekey_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit agekeys. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: agekey-editor-role 6 | rules: 7 | - apiGroups: 8 | - gitopssecret.snappcloud.io 9 | resources: 10 | - agekeys 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - gitopssecret.snappcloud.io 21 | resources: 22 | - agekeys/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/agekey_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view agekeys. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: agekey-viewer-role 6 | rules: 7 | - apiGroups: 8 | - gitopssecret.snappcloud.io 9 | resources: 10 | - agekeys 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - gitopssecret.snappcloud.io 17 | resources: 18 | - agekeys/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/agesecret_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit agesecrets. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: agesecret-editor-role 6 | rules: 7 | - apiGroups: 8 | - gitopssecret.snappcloud.io 9 | resources: 10 | - agesecrets 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - gitopssecret.snappcloud.io 21 | resources: 22 | - agesecrets/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/agesecret_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view agesecrets. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: agesecret-viewer-role 6 | rules: 7 | - apiGroups: 8 | - gitopssecret.snappcloud.io 9 | resources: 10 | - agesecrets 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - gitopssecret.snappcloud.io 17 | resources: 18 | - agesecrets/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | protocol: TCP 13 | targetPort: https 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /config/rbac/custom_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: manager-custom-role 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: 9 | - secrets 10 | verbs: 11 | - create 12 | - delete 13 | - get 14 | - list 15 | - update 16 | - watch -------------------------------------------------------------------------------- /config/rbac/custom_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-custom-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-custom-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | # custom roles and rolebindings 20 | - custom_role.yaml 21 | - custom_role_binding.yaml -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - gitopssecret.snappcloud.io 9 | resources: 10 | - agekeys 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - gitopssecret.snappcloud.io 21 | resources: 22 | - agekeys/finalizers 23 | verbs: 24 | - update 25 | - apiGroups: 26 | - gitopssecret.snappcloud.io 27 | resources: 28 | - agekeys/status 29 | verbs: 30 | - get 31 | - patch 32 | - update 33 | - apiGroups: 34 | - gitopssecret.snappcloud.io 35 | resources: 36 | - agesecrets 37 | verbs: 38 | - create 39 | - delete 40 | - get 41 | - list 42 | - patch 43 | - update 44 | - watch 45 | - apiGroups: 46 | - gitopssecret.snappcloud.io 47 | resources: 48 | - agesecrets/finalizers 49 | verbs: 50 | - update 51 | - apiGroups: 52 | - gitopssecret.snappcloud.io 53 | resources: 54 | - agesecrets/status 55 | verbs: 56 | - get 57 | - patch 58 | - update 59 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/samples/_v1alpha1_agekey.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 2 | kind: AgeKey 3 | metadata: 4 | name: agekey-sample 5 | namespace: default 6 | spec: 7 | ageSecretKey: "AGE-SECRET-KEY-1H75QW77QX00PN9EKJHCR6XFYTA9DCFWMDMHZR5H5T6VLWVKSA8TQGWSJYH" 8 | -------------------------------------------------------------------------------- /config/samples/_v1alpha1_agekey2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 2 | kind: AgeKey 3 | metadata: 4 | name: agekey-sample 5 | namespace: test-age-secret 6 | spec: 7 | ageSecretKey: "AGE-SECRET-KEY-1H75QW77QX00PN9EKJHCR6XFYTA9DCFWMDMHZR5H5T6VLWVKSA8TQGWSJYH" 8 | -------------------------------------------------------------------------------- /config/samples/_v1alpha1_agekey3.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 2 | kind: AgeKey 3 | metadata: 4 | name: agekey-sample 5 | namespace: test-age-secret 6 | spec: 7 | ageSecretKey: "AGE-SECRET-KEY-1WVWJ928E3UPWN8A69VFT889SL8UK2RUEW6DTPWG6QPV6AYJ7SLQQ5RHM8Z" 8 | -------------------------------------------------------------------------------- /config/samples/_v1alpha1_agekey_invalid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 2 | kind: AgeKey 3 | metadata: 4 | name: invalid-agekey-sample 5 | namespace: default 6 | spec: 7 | ageSecretKey: "AGE-SECRET-Kqwewqeasd" 8 | -------------------------------------------------------------------------------- /config/samples/_v1alpha1_agesecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gitopssecret.snappcloud.io/v1alpha1 2 | kind: AgeSecret 3 | metadata: 4 | name: agesecret-sample 5 | namespace: test-age-secret 6 | labels: 7 | key_label: value_label 8 | app.kubernetes.io/instance: this-should-be-removed 9 | annotations: 10 | key_annotation: value_annotation 11 | spec: 12 | labelsToRemove: 13 | - app.kubernetes.io/instance 14 | ageKeyRef: agekey-sample 15 | stringData: | 16 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXbXlPYUxHRTVMRGxBdkxr 17 | Zk5VZXpSR0Npc2EzYmRUbFNUbTVRRUpvb0ZjCjk1blA2QTFmWHN5akV1aDhGQUJR 18 | RTBwWmRvaFJjUWlYcFBBdS93bFBiaGMKLS0tIE5LZDN4aElNMEhwZXcwWW9ZUmdN 19 | bTI0V0NGWTJkTElaRmFQNjhWREQ3bXcKKHYCUSb/xvPlj5umQRFwwd1ULlXDTYXw 20 | jFRZvb9z4cXANc6Vp6kK8aoXNw0EzT46WId4KtTgCVwl7UDcgj+LXiO/e4J/2Rk0 21 | 0z1P3YUb -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /mutate-gitopssecret-snappcloud-io-v1alpha1-agekey 14 | failurePolicy: Fail 15 | name: magekey.kb.io 16 | rules: 17 | - apiGroups: 18 | - gitopssecret.snappcloud.io 19 | apiVersions: 20 | - v1alpha1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - agekeys 26 | sideEffects: None 27 | - admissionReviewVersions: 28 | - v1 29 | clientConfig: 30 | service: 31 | name: webhook-service 32 | namespace: system 33 | path: /mutate-gitopssecret-snappcloud-io-v1alpha1-agesecret 34 | failurePolicy: Fail 35 | name: magesecret.kb.io 36 | rules: 37 | - apiGroups: 38 | - gitopssecret.snappcloud.io 39 | apiVersions: 40 | - v1alpha1 41 | operations: 42 | - CREATE 43 | - UPDATE 44 | resources: 45 | - agesecrets 46 | sideEffects: None 47 | --- 48 | apiVersion: admissionregistration.k8s.io/v1 49 | kind: ValidatingWebhookConfiguration 50 | metadata: 51 | name: validating-webhook-configuration 52 | webhooks: 53 | - admissionReviewVersions: 54 | - v1 55 | clientConfig: 56 | service: 57 | name: webhook-service 58 | namespace: system 59 | path: /validate-gitopssecret-snappcloud-io-v1alpha1-agekey 60 | failurePolicy: Fail 61 | name: vagekey.kb.io 62 | rules: 63 | - apiGroups: 64 | - gitopssecret.snappcloud.io 65 | apiVersions: 66 | - v1alpha1 67 | operations: 68 | - CREATE 69 | - UPDATE 70 | resources: 71 | - agekeys 72 | sideEffects: None 73 | - admissionReviewVersions: 74 | - v1 75 | clientConfig: 76 | service: 77 | name: webhook-service 78 | namespace: system 79 | path: /validate-gitopssecret-snappcloud-io-v1alpha1-agesecret 80 | failurePolicy: Fail 81 | name: vagesecret.kb.io 82 | rules: 83 | - apiGroups: 84 | - gitopssecret.snappcloud.io 85 | apiVersions: 86 | - v1alpha1 87 | operations: 88 | - CREATE 89 | - UPDATE 90 | resources: 91 | - agesecrets 92 | sideEffects: None 93 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | protocol: TCP 11 | targetPort: 9443 12 | selector: 13 | control-plane: controller-manager 14 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | var ( 4 | ExcessAnnotations = []string{ 5 | "kubectl.kubernetes.io/last-applied-configuration", 6 | } 7 | ) 8 | -------------------------------------------------------------------------------- /controllers/agekey_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "github.com/go-logr/logr" 22 | "github.com/snapp-incubator/age-operator/k8sutils" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | "time" 25 | 26 | gitopssecretsnappcloudiov1alpha1 "github.com/snapp-incubator/age-operator/api/v1alpha1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | ) 31 | 32 | var ( 33 | err error 34 | ) 35 | 36 | // AgeKeyReconciler reconciles a AgeKey object 37 | type AgeKeyReconciler struct { 38 | client.Client 39 | Scheme *runtime.Scheme 40 | Logger logr.Logger 41 | } 42 | 43 | //+kubebuilder:rbac:groups=gitopssecret.snappcloud.io,resources=agekeys,verbs=get;list;watch;create;update;patch;delete 44 | //+kubebuilder:rbac:groups=gitopssecret.snappcloud.io,resources=agekeys/status,verbs=get;update;patch 45 | //+kubebuilder:rbac:groups=gitopssecret.snappcloud.io,resources=agekeys/finalizers,verbs=update 46 | 47 | func (r *AgeKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 48 | reqLogger := r.Logger.WithValues("Request.NamespacedName", req.NamespacedName) 49 | reqLogger.Info("Reconcile Started") 50 | 51 | ageKeyInstance := &gitopssecretsnappcloudiov1alpha1.AgeKey{} 52 | 53 | err = r.Client.Get(ctx, req.NamespacedName, ageKeyInstance) 54 | if err != nil { 55 | if errors.IsNotFound(err) { 56 | return ctrl.Result{}, nil 57 | } 58 | return ctrl.Result{Requeue: true, RequeueAfter: 20 * time.Second}, err 59 | } 60 | 61 | if err = k8sutils.HandleAgeKeyFinalizers(ageKeyInstance, r.Client); err != nil { 62 | return ctrl.Result{}, err 63 | } 64 | 65 | if err = k8sutils.AddAgeKeyFinalizers(ageKeyInstance, r.Client); err != nil { 66 | return ctrl.Result{}, err 67 | } 68 | 69 | if err = k8sutils.ValidateAgeKey(ageKeyInstance, r.Client); err != nil { 70 | return ctrl.Result{}, err 71 | } 72 | 73 | if err = k8sutils.CreateAgeKeyFile(ageKeyInstance); err != nil { 74 | return ctrl.Result{}, err 75 | } 76 | 77 | return ctrl.Result{}, nil 78 | } 79 | 80 | // SetupWithManager sets up the controller with the Manager. 81 | func (r *AgeKeyReconciler) SetupWithManager(mgr ctrl.Manager) error { 82 | return ctrl.NewControllerManagedBy(mgr). 83 | For(&gitopssecretsnappcloudiov1alpha1.AgeKey{}). 84 | Complete(r) 85 | } 86 | -------------------------------------------------------------------------------- /controllers/agekey_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | "github.com/snapp-incubator/age-operator/api/v1alpha1" 8 | "github.com/snapp-incubator/age-operator/k8sutils" 9 | "k8s.io/client-go/kubernetes/scheme" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | ) 14 | 15 | var ( 16 | fooValidAgeKeyPath = filepath.Join("..", "config", "samples", "_v1alpha1_agekey.yaml") 17 | fooInvalidAgeKeyPath = filepath.Join("..", "config", "samples", "_v1alpha1_agekey_invalid.yaml") 18 | ) 19 | 20 | var _ = Describe("Testing AgeKey", func() { 21 | ctx := context.Background() 22 | validAgeKeyObj := &v1alpha1.AgeKey{} 23 | invalidAgeKeyObj := &v1alpha1.AgeKey{} 24 | 25 | BeforeEach(func() { 26 | content, err := os.ReadFile(fooValidAgeKeyPath) 27 | Expect(err).Should(BeNil()) 28 | 29 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(content, nil, nil) 30 | validAgeKeyObj = obj.(*v1alpha1.AgeKey) 31 | Expect(err).Should(BeNil()) 32 | }) 33 | 34 | BeforeEach(func() { 35 | content, err := os.ReadFile(fooInvalidAgeKeyPath) 36 | Expect(err).Should(BeNil()) 37 | 38 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(content, nil, nil) 39 | invalidAgeKeyObj = obj.(*v1alpha1.AgeKey) 40 | Expect(err).Should(BeNil()) 41 | }) 42 | 43 | Context("When creating AgeKey", func() { 44 | It("should fail if AgeKey is invalid", func() { 45 | err = k8sClient.Create(ctx, invalidAgeKeyObj) 46 | Expect(err).To(BeNil()) 47 | err = k8sClient.Delete(ctx, invalidAgeKeyObj) 48 | Expect(err).To(BeNil()) 49 | }) 50 | 51 | It("should pass if AgeKey is valid", func() { 52 | err = k8sClient.Create(ctx, validAgeKeyObj) 53 | Expect(err).To(BeNil()) 54 | time.Sleep(time.Second * 2) 55 | 56 | // check that file is created successfully 57 | _, err = os.Stat(k8sutils.GenerateAgeKeyFullPath(validAgeKeyObj)) 58 | Expect(err).To(BeNil()) 59 | 60 | err = k8sClient.Delete(ctx, validAgeKeyObj) 61 | Expect(err).To(BeNil()) 62 | time.Sleep(time.Second * 2) 63 | 64 | // check that file is removed successfully 65 | _, err = os.Stat(k8sutils.GenerateAgeKeyFullPath(validAgeKeyObj)) 66 | Expect(os.IsNotExist(err)).To(BeTrue()) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /controllers/agesecret_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "github.com/go-logr/logr" 22 | "github.com/snapp-incubator/age-operator/k8sutils" 23 | "github.com/snapp-incubator/age-operator/lang" 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/api/errors" 26 | "time" 27 | 28 | gitopssecretsnappcloudiov1alpha1 "github.com/snapp-incubator/age-operator/api/v1alpha1" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | ) 33 | 34 | var ( 35 | requeueAfterTime = 1 * time.Minute 36 | ) 37 | 38 | // AgeSecretReconciler reconciles a AgeSecret object 39 | type AgeSecretReconciler struct { 40 | client.Client 41 | Scheme *runtime.Scheme 42 | Logger logr.Logger 43 | } 44 | 45 | //+kubebuilder:rbac:groups=gitopssecret.snappcloud.io,resources=agesecrets,verbs=get;list;watch;create;update;patch;delete 46 | //+kubebuilder:rbac:groups=gitopssecret.snappcloud.io,resources=agesecrets/status,verbs=get;update;patch 47 | //+kubebuilder:rbac:groups=gitopssecret.snappcloud.io,resources=agesecrets/finalizers,verbs=update 48 | 49 | func (r *AgeSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 50 | reqLogger := r.Logger.WithValues("Request.NamespacedName", req.NamespacedName) 51 | reqLogger.Info("Reconcile Started") 52 | 53 | ageSecretInstance := &gitopssecretsnappcloudiov1alpha1.AgeSecret{} 54 | 55 | if err = r.Client.Get(ctx, req.NamespacedName, ageSecretInstance); err != nil { 56 | if errors.IsNotFound(err) { 57 | return ctrl.Result{}, nil 58 | } 59 | return ctrl.Result{}, err 60 | } 61 | 62 | goOn, err := k8sutils.HandleAgeSecretFinalizers(ageSecretInstance, r.Client) 63 | if err != nil { 64 | reqLogger.Info("could not handle AgeSecret finalizers", "namespace", ageSecretInstance.GetNamespace(), "name", ageSecretInstance.GetName(), "error", err) 65 | return ctrl.Result{Requeue: true, RequeueAfter: requeueAfterTime}, err 66 | } 67 | if !goOn { 68 | return ctrl.Result{}, nil 69 | } 70 | 71 | if err = k8sutils.AddAgeSecretFinalizers(ageSecretInstance, r.Client); err != nil { 72 | reqLogger.Info("could not add required finalizers to AgeSecret", "namespace", ageSecretInstance.GetNamespace(), "name", ageSecretInstance.GetName(), "error", err) 73 | return ctrl.Result{Requeue: true, RequeueAfter: requeueAfterTime}, err 74 | } 75 | 76 | if ageSecretInstance.Spec.Suspend { 77 | reqLogger.Info("AgeSecret suspended", "namespace", ageSecretInstance.GetNamespace(), "name", ageSecretInstance.GetName()) 78 | return ctrl.Result{}, nil 79 | } 80 | 81 | refAgeKey, errAgeKey := k8sutils.CheckAgeKeyReference(ageSecretInstance, r.Client) 82 | if errAgeKey != nil { 83 | reqLogger.Info("could not fetch AgeSecret referenced AgeKey", "namespace", ageSecretInstance.GetNamespace(), "name", ageSecretInstance.GetName(), "error", errAgeKey) 84 | return ctrl.Result{Requeue: true, RequeueAfter: requeueAfterTime}, errAgeKey 85 | } 86 | 87 | decryptedStringData, errDecrypt := k8sutils.DecryptAgeSecret(ageSecretInstance, r.Client, refAgeKey) 88 | if errDecrypt != nil { 89 | reqLogger.Info("could not decrypt AgeSecret", "namespace", ageSecretInstance.GetNamespace(), "name", ageSecretInstance.GetName(), "error", errDecrypt) 90 | return ctrl.Result{}, errDecrypt 91 | } 92 | 93 | err = k8sutils.CreateChildFromAgeSecret(ageSecretInstance, r.Client, decryptedStringData) 94 | if err != nil { 95 | reqLogger.Info("could not create child secret from AgeSecret", "namespace", ageSecretInstance.GetNamespace(), "name", ageSecretInstance.GetName(), "error", err) 96 | return ctrl.Result{}, err 97 | } 98 | 99 | ageSecretInstance.Status.Health = lang.AgeSecretStatusHealthy 100 | _ = r.Status().Update(ctx, ageSecretInstance) 101 | return ctrl.Result{}, nil 102 | } 103 | 104 | // SetupWithManager sets up the controller with the Manager. 105 | func (r *AgeSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 106 | return ctrl.NewControllerManagedBy(mgr). 107 | For(&gitopssecretsnappcloudiov1alpha1.AgeSecret{}). 108 | Owns(&corev1.Secret{}). 109 | Complete(r) 110 | } 111 | -------------------------------------------------------------------------------- /controllers/agesecret_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | "github.com/snapp-incubator/age-operator/api/v1alpha1" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/api/errors" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | "k8s.io/client-go/kubernetes/scheme" 13 | "os" 14 | "path/filepath" 15 | "time" 16 | ) 17 | 18 | var ( 19 | fooValidAgeKeyPath3 = filepath.Join("..", "config", "samples", "_v1alpha1_agekey3.yaml") 20 | fooValidAgeKeyPath2 = filepath.Join("..", "config", "samples", "_v1alpha1_agekey2.yaml") 21 | fooValidAgeSecretPath = filepath.Join("..", "config", "samples", "_v1alpha1_agesecret.yaml") 22 | ) 23 | 24 | var _ = Describe("", Serial, func() { 25 | ctx := context.Background() 26 | validAgeKeyObj := &v1alpha1.AgeKey{} 27 | invalidAgeKeyObj := &v1alpha1.AgeKey{} 28 | validAgeSecretObj := &v1alpha1.AgeSecret{} 29 | namespaceObj := &corev1.Namespace{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: "test-age-secret", 32 | }, 33 | } 34 | 35 | BeforeEach(func() { 36 | content, err := os.ReadFile(fooValidAgeKeyPath2) 37 | Expect(err).Should(BeNil()) 38 | 39 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(content, nil, nil) 40 | validAgeKeyObj = obj.(*v1alpha1.AgeKey) 41 | Expect(err).Should(BeNil()) 42 | }) 43 | 44 | BeforeEach(func() { 45 | content, err := os.ReadFile(fooValidAgeKeyPath3) 46 | Expect(err).Should(BeNil()) 47 | 48 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(content, nil, nil) 49 | invalidAgeKeyObj = obj.(*v1alpha1.AgeKey) 50 | Expect(err).Should(BeNil()) 51 | }) 52 | 53 | BeforeEach(func() { 54 | content, err := os.ReadFile(fooValidAgeSecretPath) 55 | Expect(err).Should(BeNil()) 56 | 57 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(content, nil, nil) 58 | validAgeSecretObj = obj.(*v1alpha1.AgeSecret) 59 | Expect(err).Should(BeNil()) 60 | }) 61 | 62 | Context("When creating AgeSecret", func() { 63 | It("should pass if AgeSecret and referenced AgeKey is valid", func() { 64 | err = k8sClient.Create(ctx, namespaceObj) 65 | Expect(err).To(BeNil()) 66 | 67 | err = k8sClient.Create(ctx, validAgeKeyObj) 68 | Expect(err).To(BeNil()) 69 | 70 | err = k8sClient.Create(ctx, validAgeSecretObj) 71 | Expect(err).To(BeNil()) 72 | time.Sleep(time.Second * 3) 73 | 74 | fooSecretObj := &corev1.Secret{} 75 | err = k8sClient.Get(ctx, types.NamespacedName{Namespace: validAgeSecretObj.Namespace, Name: validAgeSecretObj.Name}, fooSecretObj) 76 | Expect(err).To(BeNil()) 77 | Expect(fooSecretObj.GetAnnotations()).Should(Equal(validAgeSecretObj.GetAnnotations())) 78 | 79 | // make sure unwanted label is removed 80 | unwantedLabelExists := false 81 | secretLabels := fooSecretObj.GetLabels() 82 | for _, label := range secretLabels { 83 | for _, unwantedLabel := range validAgeSecretObj.Spec.LabelsToRemove { 84 | if label == unwantedLabel { 85 | unwantedLabelExists = true 86 | break 87 | } 88 | } 89 | if unwantedLabelExists { 90 | break 91 | } 92 | } 93 | Expect(unwantedLabelExists).To(BeFalse()) 94 | 95 | sampleKeyValue, exists := fooSecretObj.Data["sample_key"] 96 | Expect(string(sampleKeyValue)).Should(Equal("sample_value")) 97 | Expect(exists).To(BeTrue()) 98 | 99 | testKeyValue, exists := fooSecretObj.Data["test_key"] 100 | Expect(string(testKeyValue)).Should(Equal("test_value")) 101 | Expect(exists).To(BeTrue()) 102 | 103 | err = k8sClient.Delete(ctx, validAgeKeyObj) 104 | Expect(err).To(BeNil()) 105 | 106 | err = k8sClient.Delete(ctx, validAgeSecretObj) 107 | Expect(err).To(BeNil()) 108 | time.Sleep(time.Second * 3) 109 | 110 | fooSecretObj2 := &corev1.Secret{} 111 | err = k8sClient.Get(ctx, types.NamespacedName{Namespace: validAgeSecretObj.Namespace, Name: validAgeSecretObj.Name}, fooSecretObj2) 112 | Expect(err).NotTo(BeNil()) 113 | Expect(errors.IsNotFound(err)).To(BeTrue()) 114 | }) 115 | 116 | It("should fail if AgeSecret is valid but referenced AgeKey is not the recipient", func() { 117 | err = k8sClient.Create(ctx, invalidAgeKeyObj) 118 | Expect(err).To(BeNil()) 119 | 120 | err = k8sClient.Create(ctx, validAgeSecretObj) 121 | Expect(err).To(BeNil()) 122 | time.Sleep(time.Second * 3) 123 | 124 | fooSecretObj := &corev1.Secret{} 125 | err = k8sClient.Get(ctx, types.NamespacedName{Namespace: validAgeSecretObj.Namespace, Name: validAgeSecretObj.Name}, fooSecretObj) 126 | Expect(err).NotTo(BeNil()) 127 | Expect(errors.IsNotFound(err)).To(BeTrue()) 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "k8s.io/client-go/kubernetes" 21 | "path/filepath" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | "testing" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/envtest" 31 | logf "sigs.k8s.io/controller-runtime/pkg/log" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | gitopssecretsnappcloudiov1alpha1 "github.com/snapp-incubator/age-operator/api/v1alpha1" 35 | //+kubebuilder:scaffold:imports 36 | ) 37 | 38 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 39 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 40 | 41 | var cfg *rest.Config 42 | var k8sClient client.Client 43 | var testEnv *envtest.Environment 44 | 45 | func TestAPIs(t *testing.T) { 46 | RegisterFailHandler(Fail) 47 | RunSpecs(t, "Controller Suite") 48 | } 49 | 50 | var _ = BeforeSuite(func() { 51 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 52 | 53 | By("bootstrapping test environment") 54 | testEnv = &envtest.Environment{ 55 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 56 | ErrorIfCRDPathMissing: true, 57 | } 58 | 59 | var err error 60 | // cfg is defined in this file globally. 61 | cfg, err = testEnv.Start() 62 | Expect(err).NotTo(HaveOccurred()) 63 | Expect(cfg).NotTo(BeNil()) 64 | 65 | err = gitopssecretsnappcloudiov1alpha1.AddToScheme(scheme.Scheme) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | //+kubebuilder:scaffold:scheme 69 | 70 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(k8sClient).NotTo(BeNil()) 73 | 74 | K8sClientset, err := kubernetes.NewForConfig(cfg) 75 | Expect(err).NotTo(HaveOccurred()) 76 | Expect(K8sClientset).NotTo(BeNil()) 77 | 78 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 79 | Scheme: scheme.Scheme, 80 | }) 81 | Expect(err).ToNot(HaveOccurred()) 82 | Expect(k8sManager).NotTo(BeNil()) 83 | 84 | err = (&AgeKeyReconciler{ 85 | Client: k8sManager.GetClient(), 86 | Scheme: k8sManager.GetScheme(), 87 | Logger: ctrl.Log.WithName("controllers").WithName("AgeKey"), 88 | }).SetupWithManager(k8sManager) 89 | Expect(err).ToNot(HaveOccurred()) 90 | 91 | err = (&AgeSecretReconciler{ 92 | Client: k8sManager.GetClient(), 93 | Scheme: k8sManager.GetScheme(), 94 | Logger: ctrl.Log.WithName("controllers").WithName("AgeSecret"), 95 | }).SetupWithManager(k8sManager) 96 | Expect(err).ToNot(HaveOccurred()) 97 | 98 | go func() { 99 | defer GinkgoRecover() 100 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 101 | Expect(err).ToNot(HaveOccurred()) 102 | }() 103 | }) 104 | 105 | var _ = AfterSuite(func() { 106 | By("tearing down the test environment") 107 | _ = testEnv.Stop() 108 | err := testEnv.Stop() 109 | Expect(err).NotTo(HaveOccurred()) 110 | }) 111 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/snapp-incubator/age-operator 2 | 3 | go 1.23 4 | 5 | require ( 6 | filippo.io/age v1.2.0 7 | github.com/go-logr/logr v1.4.2 8 | github.com/onsi/ginkgo/v2 v2.21.0 9 | github.com/onsi/gomega v1.35.1 10 | k8s.io/api v0.31.2 11 | k8s.io/apimachinery v0.31.2 12 | k8s.io/client-go v0.31.2 13 | sigs.k8s.io/controller-runtime v0.19.1 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 21 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 22 | github.com/fsnotify/fsnotify v1.7.0 // indirect 23 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 24 | github.com/go-logr/zapr v1.3.0 // indirect 25 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 26 | github.com/go-openapi/jsonreference v0.21.0 // indirect 27 | github.com/go-openapi/swag v0.23.0 // indirect 28 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/gnostic-models v0.6.8 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/gofuzz v1.2.0 // indirect 35 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 36 | github.com/google/uuid v1.6.0 // indirect 37 | github.com/imdario/mergo v0.3.16 // indirect 38 | github.com/josharian/intern v1.0.0 // indirect 39 | github.com/json-iterator/go v1.1.12 // indirect 40 | github.com/klauspost/compress v1.17.9 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v1.0.2 // indirect 44 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 45 | github.com/pkg/errors v0.9.1 // indirect 46 | github.com/prometheus/client_golang v1.20.2 // indirect 47 | github.com/prometheus/client_model v0.6.1 // indirect 48 | github.com/prometheus/common v0.55.0 // indirect 49 | github.com/prometheus/procfs v0.15.1 // indirect 50 | github.com/spf13/pflag v1.0.5 // indirect 51 | github.com/x448/float16 v0.8.4 // indirect 52 | go.uber.org/multierr v1.11.0 // indirect 53 | go.uber.org/zap v1.27.0 // indirect 54 | golang.org/x/crypto v0.28.0 // indirect 55 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect 56 | golang.org/x/net v0.30.0 // indirect 57 | golang.org/x/oauth2 v0.22.0 // indirect 58 | golang.org/x/sys v0.26.0 // indirect 59 | golang.org/x/term v0.25.0 // indirect 60 | golang.org/x/text v0.19.0 // indirect 61 | golang.org/x/time v0.6.0 // indirect 62 | golang.org/x/tools v0.26.0 // indirect 63 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 64 | google.golang.org/protobuf v1.35.1 // indirect 65 | gopkg.in/inf.v0 v0.9.1 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | k8s.io/apiextensions-apiserver v0.31.0 // indirect 69 | k8s.io/klog/v2 v2.130.1 // indirect 70 | k8s.io/kube-openapi v0.0.0-20240822171749-76de80e0abd9 // indirect 71 | k8s.io/utils v0.0.0-20240821151609-f90d01438635 // indirect 72 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 73 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 74 | sigs.k8s.io/yaml v1.4.0 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= 2 | c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= 3 | filippo.io/age v1.2.0 h1:vRDp7pUMaAJzXNIWJVAZnEf/Dyi4Vu4wI8S1LBzufhE= 4 | filippo.io/age v1.2.0/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= 14 | github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 15 | github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= 16 | github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 17 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 18 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 19 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 20 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 21 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 22 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 23 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 24 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 26 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 27 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 28 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 29 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 30 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 31 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 32 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 33 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 34 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 35 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 36 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 39 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 40 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 41 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 42 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 43 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 44 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 45 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 48 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 49 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 50 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 51 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 52 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 53 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 54 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 55 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 56 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 57 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 58 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 59 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 60 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 61 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 62 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 63 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 64 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 65 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 66 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 67 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 68 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 69 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 70 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 71 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 73 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 75 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 76 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 77 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 78 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 79 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 80 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 81 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 82 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 83 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 85 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 86 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= 88 | github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 89 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 90 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 91 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 92 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 93 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 94 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 95 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 96 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 97 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 98 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 99 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 100 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 101 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 102 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 103 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 104 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 105 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 106 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 107 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 108 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 109 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 110 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 111 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 112 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 113 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 114 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 115 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 116 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 117 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 118 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= 119 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 120 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 121 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 122 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 123 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 124 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 125 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 126 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 127 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 128 | golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= 129 | golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 130 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 134 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 137 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 138 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 139 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 140 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 141 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 142 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 143 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 144 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 145 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 146 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 147 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 148 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 149 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 150 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 151 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 152 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 153 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 154 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 155 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 156 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 157 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 158 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 159 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 160 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 161 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 162 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 163 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 164 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 165 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 166 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 167 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 168 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 169 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 170 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 171 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 172 | k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= 173 | k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= 174 | k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= 175 | k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= 176 | k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= 177 | k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= 178 | k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= 179 | k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= 180 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 181 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 182 | k8s.io/kube-openapi v0.0.0-20240822171749-76de80e0abd9 h1:y+4z/s0h3R97P/o/098DSjlpyNpHzGirNPlTL+GHdqY= 183 | k8s.io/kube-openapi v0.0.0-20240822171749-76de80e0abd9/go.mod h1:s4yb9FXajAVNRnxSB5Ckpr/oq2LP4mKSMWeZDVppd30= 184 | k8s.io/utils v0.0.0-20240821151609-f90d01438635 h1:2wThSvJoW/Ncn9TmQEYXRnevZXi2duqHWf5OX9S3zjI= 185 | k8s.io/utils v0.0.0-20240821151609-f90d01438635/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 186 | sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= 187 | sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= 188 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 189 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 190 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 191 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 192 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 193 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 194 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /k8sutils/agekey.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "context" 5 | "filippo.io/age" 6 | "fmt" 7 | "github.com/snapp-incubator/age-operator/api/v1alpha1" 8 | "os" 9 | "path/filepath" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | AgeKeysRootPath = "/tmp/keys/" 16 | ) 17 | 18 | func finalizeAgeKey(ageKey *v1alpha1.AgeKey) error { 19 | logger := finalizerLogger(ageKey.GetNamespace(), AgeKeyFinalizer) 20 | fullPath := GenerateAgeKeyFullPath(ageKey) 21 | if _, err := os.Stat(fullPath); err != nil { 22 | if os.IsNotExist(err) { 23 | return nil 24 | } 25 | logger.Error(err, "Could not get stats of file", ageKey.GetName()) 26 | return fmt.Errorf("could not finalize") 27 | } 28 | if err := os.Remove(fullPath); err != nil { 29 | logger.Error(err, "Could not remove file", ageKey.GetName()) 30 | return fmt.Errorf("could not finalize") 31 | } 32 | return nil 33 | } 34 | 35 | func CreateAgeKeyFile(ageKey *v1alpha1.AgeKey) error { 36 | logger := NewLogger(ageKey.GetNamespace(), ageKey.GetName()) 37 | fullPath := GenerateAgeKeyFullPath(ageKey) 38 | if _, err := os.Stat(fullPath); err != nil { 39 | if os.IsNotExist(err) { 40 | parentDir := GenerateAgeKeyParentDir(ageKey) 41 | if errDirCreation := os.MkdirAll(parentDir, os.ModePerm); errDirCreation != nil { 42 | logger.Error(err, "could not create directory", "path", ageKey) 43 | return errDirCreation 44 | } 45 | if _, errCreate := os.Create(fullPath); errCreate != nil { 46 | return errCreate 47 | } 48 | } else { 49 | logger.Error(err, "could not stat file for creating") 50 | return err 51 | } 52 | } 53 | return writeAgeKeyToFile(fullPath, ageKey) 54 | } 55 | 56 | func writeAgeKeyToFile(fullPath string, ageKey *v1alpha1.AgeKey) error { 57 | var file, err = os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 58 | if err != nil { 59 | return err 60 | } 61 | defer func() { 62 | _ = file.Close() 63 | }() 64 | 65 | _, err = file.WriteString(strings.TrimSpace(ageKey.Spec.AgeSecretKey)) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | err = file.Sync() 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | 77 | func ValidateAgeKey(ageKey *v1alpha1.AgeKey, k8sclient client.Client) error { 78 | logger := NewLogger(ageKey.GetNamespace(), ageKey.GetName()) 79 | if _, err := age.ParseX25519Identity(ageKey.Spec.AgeSecretKey); err != nil { 80 | logger.Error(err, "invalid agekey on reconcile") 81 | ageKey.Status.Message = "Invalid AgeKey" 82 | _ = k8sclient.Status().Update(context.Background(), ageKey) 83 | return fmt.Errorf("invalid AgeKey on field .Spec.ageSecretKey") 84 | } 85 | return nil 86 | } 87 | 88 | func GenerateAgeKeyFullPath(ageKey *v1alpha1.AgeKey) string { 89 | return filepath.Join(GenerateAgeKeyParentDir(ageKey), ageKey.GetName()) 90 | } 91 | 92 | func GenerateAgeKeyParentDir(ageKey *v1alpha1.AgeKey) string { 93 | return filepath.Join(AgeKeysRootPath, ageKey.GetNamespace()) 94 | } 95 | 96 | func UpdateAgeKeyStatus(ageKey *v1alpha1.AgeKey, k8sclient client.Client, msg string) error { 97 | ageKey.Status.Message = msg 98 | return k8sclient.Status().Update(context.Background(), ageKey) 99 | } 100 | -------------------------------------------------------------------------------- /k8sutils/agesecret.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "filippo.io/age" 7 | "filippo.io/age/armor" 8 | "github.com/snapp-incubator/age-operator/api/v1alpha1" 9 | "github.com/snapp-incubator/age-operator/consts" 10 | "github.com/snapp-incubator/age-operator/lang" 11 | "io" 12 | corev1 "k8s.io/api/core/v1" 13 | apiequality "k8s.io/apimachinery/pkg/api/equality" 14 | "k8s.io/apimachinery/pkg/api/errors" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/types" 17 | "k8s.io/apimachinery/pkg/util/yaml" 18 | "sigs.k8s.io/controller-runtime/pkg/client" 19 | "strings" 20 | ) 21 | 22 | func finalizeAgeSecret(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client) error { 23 | logger := finalizerLogger(ageSecret.GetNamespace(), AgeSecretFinalizer) 24 | childSecret := &corev1.Secret{} 25 | err := k8sclient.Get(context.Background(), types.NamespacedName{Name: ageSecret.GetName(), Namespace: ageSecret.GetNamespace()}, childSecret) 26 | if err != nil { 27 | if !errors.IsNotFound(err) { 28 | logger.Error(err, "Could not get child secret"+ageSecret.GetName()) 29 | return err 30 | } else { 31 | return nil 32 | } 33 | } 34 | if err = k8sclient.Delete(context.Background(), childSecret); err != nil && !errors.IsNotFound(err) { 35 | logger.Error(err, "Could not delete child secret "+ageSecret.GetName()) 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func CreateChildFromAgeSecret(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client, rawStringData map[string]string) error { 42 | childSecretLabels := cloneLabels(ageSecret.GetLabels(), ageSecret.Spec.LabelsToRemove) 43 | childSecretAnnotations := cloneAnnotations(ageSecret.GetAnnotations()) 44 | childSecretMetaObj := metav1.ObjectMeta{ 45 | Name: ageSecret.GetName(), 46 | Namespace: ageSecret.GetNamespace(), 47 | Labels: childSecretLabels, 48 | Annotations: childSecretAnnotations, 49 | } 50 | secretObj := &corev1.Secret{ 51 | ObjectMeta: childSecretMetaObj, 52 | Type: corev1.SecretTypeOpaque, 53 | StringData: rawStringData, 54 | } 55 | return CreateOrUpdateSecretObj(ageSecret, secretObj, k8sclient) 56 | } 57 | 58 | func CreateOrUpdateSecretObj(ageSecret *v1alpha1.AgeSecret, secret *corev1.Secret, k8sclient client.Client) error { 59 | secretToLoad := &corev1.Secret{} 60 | logger := NewLogger(ageSecret.GetNamespace(), ageSecret.GetName()) 61 | err := k8sclient.Get(context.Background(), types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name}, secretToLoad) 62 | if err != nil { 63 | if errors.IsNotFound(err) { 64 | logger.Info("secret not found, creating...", "namespace", secret.Namespace, "secretName", secret.Name) 65 | errCreateChildSecret := k8sclient.Create(context.Background(), secret) 66 | if errCreateChildSecret != nil { 67 | ageSecret.Status.Health = lang.AgeSecretStatusUnhealthy 68 | ageSecret.Status.Message = "could not create child secret" 69 | _ = k8sclient.Status().Update(context.Background(), ageSecret) 70 | return errCreateChildSecret 71 | } 72 | ageSecret.Status.Health = lang.AgeSecretStatusHealthy 73 | _ = k8sclient.Status().Update(context.Background(), ageSecret) 74 | return nil 75 | } 76 | ageSecret.Status.Health = lang.AgeSecretStatusUnhealthy 77 | ageSecret.Status.Message = "could not fetch child secret" 78 | _ = k8sclient.Status().Update(context.Background(), ageSecret) 79 | return err 80 | } 81 | 82 | if !apiequality.Semantic.DeepEqual(secretToLoad, secret) { 83 | logger.Info("child secret exists but needs to get refreshed") 84 | err = k8sclient.Update(context.Background(), secret) 85 | if err != nil { 86 | logger.Error(err, "could not refresh child secret") 87 | ageSecret.Status.Health = lang.AgeSecretStatusUnhealthy 88 | ageSecret.Status.Message = "could not refresh child secret" 89 | _ = k8sclient.Status().Update(context.Background(), ageSecret) 90 | return err 91 | } 92 | } 93 | if ageSecret.Status.Health != lang.AgeSecretStatusHealthy { 94 | ageSecret.Status.Health = lang.AgeSecretStatusHealthy 95 | errUpdateHealth := k8sclient.Status().Update(context.Background(), ageSecret) 96 | logger.Error(errUpdateHealth, "Could not update status of ageSecret", "namespace", ageSecret.GetNamespace(), "name", ageSecret.GetName()) 97 | } 98 | return nil 99 | } 100 | 101 | func DecryptAgeSecret(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client, ageKey *v1alpha1.AgeKey) (map[string]string, error) { 102 | logger := NewLogger(ageSecret.GetNamespace(), ageSecret.GetName()) 103 | identity, err := age.ParseX25519Identity(ageKey.Spec.AgeSecretKey) 104 | if err != nil { 105 | return nil, err 106 | } 107 | encryptedData := "-----BEGIN AGE ENCRYPTED FILE-----\n" + strings.TrimSpace(ageSecret.Spec.StringData) + "\n-----END AGE ENCRYPTED FILE-----" 108 | reader, errDecrypt := age.Decrypt(armor.NewReader(strings.NewReader(encryptedData)), identity) 109 | if errDecrypt != nil { 110 | logger.Error(errDecrypt, "could not decrypt with given AgeKey") 111 | return nil, errDecrypt 112 | } 113 | output := &bytes.Buffer{} 114 | if _, err := io.Copy(output, reader); err != nil { 115 | return nil, err 116 | } 117 | var rawStringData map[string]string 118 | outputBytes := output.Bytes() 119 | err = yaml.Unmarshal(outputBytes, &rawStringData) 120 | if err != nil { 121 | logger.Error(err, "given yaml structure is not valid") 122 | ageSecret.Status.Message = "given yaml structure is not valid" 123 | _ = k8sclient.Update(context.Background(), ageSecret) 124 | return nil, err 125 | } 126 | return rawStringData, nil 127 | } 128 | 129 | func CheckAgeKeyReference(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client) (*v1alpha1.AgeKey, error) { 130 | ageKeyObj := &v1alpha1.AgeKey{} 131 | err := k8sclient.Get(context.Background(), types.NamespacedName{Namespace: ageSecret.Namespace, Name: ageSecret.Spec.AgeKeyRef}, ageKeyObj) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return ageKeyObj, nil 136 | } 137 | 138 | func cloneLabels(labels map[string]string, labelsToRemove []string) map[string]string { 139 | tmpLabels := cloneMap(labels) 140 | for _, label := range labelsToRemove { 141 | delete(tmpLabels, label) 142 | } 143 | return tmpLabels 144 | } 145 | 146 | func cloneAnnotations(annotations map[string]string) map[string]string { 147 | tmpAnnotations := cloneMap(annotations) 148 | for _, annotation := range consts.ExcessAnnotations { 149 | delete(tmpAnnotations, annotation) 150 | } 151 | return tmpAnnotations 152 | } 153 | 154 | func cloneMap(oldMap map[string]string) map[string]string { 155 | newMap := make(map[string]string) 156 | 157 | for key, value := range oldMap { 158 | newMap[key] = value 159 | } 160 | 161 | return newMap 162 | } 163 | 164 | func UpdateAgeSecretStatus(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client, health, msg string) error { 165 | ageSecret.Status.Health = health 166 | ageSecret.Status.Message = msg 167 | return k8sclient.Status().Update(context.Background(), ageSecret) 168 | } 169 | -------------------------------------------------------------------------------- /k8sutils/finalizer.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "context" 5 | "github.com/go-logr/logr" 6 | "github.com/snapp-incubator/age-operator/api/v1alpha1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 9 | logf "sigs.k8s.io/controller-runtime/pkg/log" 10 | ) 11 | 12 | const ( 13 | AgeKeyFinalizer string = "AgeKeyFinalizer" 14 | AgeSecretFinalizer string = "AgeSecretFinalizer" 15 | ) 16 | 17 | var logObj = logf.Log.WithName("controller") 18 | 19 | func finalizerLogger(namespace string, name string) logr.Logger { 20 | reqLogger := logObj.WithValues("Request.Service.Namespace", namespace, "Request.Finalizer.Name", name) 21 | return reqLogger 22 | } 23 | 24 | func NewLogger(namespace string, name string) logr.Logger { 25 | reqLogger := logObj.WithValues("Request.Namespace", namespace, "Request.Name", name) 26 | return reqLogger 27 | } 28 | 29 | func HandleAgeKeyFinalizers(ageKey *v1alpha1.AgeKey, k8sclient client.Client) error { 30 | if ageKey.GetDeletionTimestamp() != nil { 31 | logger := finalizerLogger(ageKey.GetNamespace(), AgeKeyFinalizer) 32 | if controllerutil.ContainsFinalizer(ageKey, AgeKeyFinalizer) { 33 | if err := finalizeAgeKey(ageKey); err != nil { 34 | logger.Error(err, "Could not execute finalizer") 35 | return err 36 | } 37 | controllerutil.RemoveFinalizer(ageKey, AgeKeyFinalizer) 38 | if err := k8sclient.Update(context.Background(), ageKey); err != nil { 39 | logger.Error(err, "Could not remove finalizer "+AgeKeyFinalizer) 40 | return err 41 | } 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func HandleAgeSecretFinalizers(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client) (bool, error) { 48 | if ageSecret.GetDeletionTimestamp() != nil { 49 | logger := finalizerLogger(ageSecret.GetNamespace(), AgeSecretFinalizer) 50 | if controllerutil.ContainsFinalizer(ageSecret, AgeSecretFinalizer) { 51 | if err := finalizeAgeSecret(ageSecret, k8sclient); err != nil { 52 | return false, err 53 | } 54 | controllerutil.RemoveFinalizer(ageSecret, AgeSecretFinalizer) 55 | if err := k8sclient.Update(context.Background(), ageSecret); err != nil { 56 | logger.Error(err, "Could not remove finalizer "+AgeSecretFinalizer) 57 | return false, err 58 | } 59 | } 60 | return false, nil 61 | } 62 | return true, nil 63 | } 64 | 65 | func AddAgeKeyFinalizers(ageKey *v1alpha1.AgeKey, k8sclient client.Client) error { 66 | if !controllerutil.ContainsFinalizer(ageKey, AgeKeyFinalizer) { 67 | controllerutil.AddFinalizer(ageKey, AgeKeyFinalizer) 68 | return k8sclient.Update(context.Background(), ageKey) 69 | } 70 | return nil 71 | } 72 | 73 | func AddAgeSecretFinalizers(ageSecret *v1alpha1.AgeSecret, k8sclient client.Client) error { 74 | if !controllerutil.ContainsFinalizer(ageSecret, AgeSecretFinalizer) { 75 | controllerutil.AddFinalizer(ageSecret, AgeSecretFinalizer) 76 | return k8sclient.Update(context.Background(), ageSecret) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /lang/lang.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | const ( 4 | AgeSecretStatusHealthy = "Healthy" 5 | AgeSecretStatusUnhealthy = "Unhealthy" 6 | ) 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | gitopssecretsnappcloudiov1alpha1 "github.com/snapp-incubator/age-operator/api/v1alpha1" 28 | "github.com/snapp-incubator/age-operator/controllers" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 31 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/healthz" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 36 | //+kubebuilder:scaffold:imports 37 | ) 38 | 39 | var ( 40 | scheme = runtime.NewScheme() 41 | setupLog = ctrl.Log.WithName("setup") 42 | ) 43 | 44 | func init() { 45 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 46 | 47 | utilruntime.Must(gitopssecretsnappcloudiov1alpha1.AddToScheme(scheme)) 48 | //+kubebuilder:scaffold:scheme 49 | } 50 | 51 | func main() { 52 | var metricsAddr string 53 | var enableLeaderElection bool 54 | var probeAddr string 55 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 56 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 57 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 58 | "Enable leader election for controller manager. "+ 59 | "Enabling this will ensure there is only one active controller manager.") 60 | opts := zap.Options{ 61 | Development: true, 62 | } 63 | opts.BindFlags(flag.CommandLine) 64 | flag.Parse() 65 | 66 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 67 | 68 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 69 | Scheme: scheme, 70 | Metrics: server.Options{BindAddress: metricsAddr}, 71 | HealthProbeBindAddress: probeAddr, 72 | LeaderElection: enableLeaderElection, 73 | LeaderElectionID: "48ffd8e3.gitopssecret.snappcloud.io", 74 | }) 75 | if err != nil { 76 | setupLog.Error(err, "unable to start manager") 77 | os.Exit(1) 78 | } 79 | 80 | if err = (&controllers.AgeKeyReconciler{ 81 | Client: mgr.GetClient(), 82 | Scheme: mgr.GetScheme(), 83 | Logger: ctrl.Log.WithName("controllers").WithName("AgeKey"), 84 | }).SetupWithManager(mgr); err != nil { 85 | setupLog.Error(err, "unable to create controller", "controller", "AgeKey") 86 | os.Exit(1) 87 | } 88 | if err = (&controllers.AgeSecretReconciler{ 89 | Client: mgr.GetClient(), 90 | Scheme: mgr.GetScheme(), 91 | Logger: ctrl.Log.WithName("controllers").WithName("AgeSecret"), 92 | }).SetupWithManager(mgr); err != nil { 93 | setupLog.Error(err, "unable to create controller", "controller", "AgeSecret") 94 | os.Exit(1) 95 | } 96 | if os.Getenv("ENABLE_WEBHOOKS") != "false" { 97 | if err = (&gitopssecretsnappcloudiov1alpha1.AgeKey{}).SetupWebhookWithManager(mgr); err != nil { 98 | setupLog.Error(err, "unable to create webhook", "webhook", "AgeKey") 99 | os.Exit(1) 100 | } 101 | if err = (&gitopssecretsnappcloudiov1alpha1.AgeSecret{}).SetupWebhookWithManager(mgr); err != nil { 102 | setupLog.Error(err, "unable to create webhook", "webhook", "AgeSecret") 103 | os.Exit(1) 104 | } 105 | } 106 | //+kubebuilder:scaffold:builder 107 | 108 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 109 | setupLog.Error(err, "unable to set up health check") 110 | os.Exit(1) 111 | } 112 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 113 | setupLog.Error(err, "unable to set up ready check") 114 | os.Exit(1) 115 | } 116 | 117 | setupLog.Info("starting manager") 118 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 119 | setupLog.Error(err, "problem running manager") 120 | os.Exit(1) 121 | } 122 | } 123 | --------------------------------------------------------------------------------