├── .dockerignore ├── .github ├── grype.yaml ├── label-commands.json └── workflows │ ├── create-release.yml │ ├── label-issues.yml │ ├── size-label.yml │ ├── stale.yml │ ├── test.yml │ └── update-snyk.yml ├── .gitignore ├── .goreleaser.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── apis └── access-manager.io │ └── v1beta1 │ ├── common_types.go │ ├── groupversion_info.go │ ├── rbacdefinition_types.go │ ├── syncsecretdefinition_types.go │ └── zz_generated.deepcopy.go ├── config ├── crd │ ├── access-manager.io_rbacdefinitions.yaml │ └── access-manager.io_syncsecretdefinitions.yaml ├── manager │ └── manager.yaml ├── rbac │ ├── clusterrolebinding.yaml │ └── serviceaccount.yaml └── samples │ ├── clusterrolebindings.yaml │ ├── rolebindings.yaml │ └── syncsecretdefinition.yaml ├── controllers └── access-manager.io │ ├── namespace_controller.go │ ├── rbacdefinition_controller.go │ ├── secret_controller.go │ ├── serviceaccount_controller.go │ └── syncsecretdefinition_controller.go ├── docs └── api.md ├── e2e ├── integration_suite_test.go ├── integration_test.go └── test.sh ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── main.go ├── pkg ├── reconciler │ ├── common.go │ ├── reconciler.go │ ├── reconciler_test.go │ └── secret_reconciler.go └── util │ └── util.go └── renovate.json /.dockerignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | -------------------------------------------------------------------------------- /.github/grype.yaml: -------------------------------------------------------------------------------- 1 | ignore: [] 2 | -------------------------------------------------------------------------------- /.github/label-commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": [ 3 | { "command": "hold", "arg": "", "action": "add-label", "label": "hold" }, 4 | { "command": "hold", "arg": "cancel", "action": "remove-label", "label": "hold" }, 5 | { "command": "label", "arg": "(.*)", "action": "add-label", "label": "$1" }, 6 | { "command": "remove-label", "arg": "(.*)", "action": "remove-label", "label": "$1" }, 7 | { "command": "kind", "arg": "(bug|feature|documentation|test|cleanup|security)", "action": "add-label", "label": "kind/$1" }, 8 | { "command": "lifecycle", "arg": "(stale|frozen)", "action": "add-label", "label": "lifecycle/$1" }, 9 | { "command": "remove-lifecycle", "arg": "(stale|frozen)", "action": "remove-label", "label": "lifecycle/$1" } 10 | ], 11 | "allowedUsers": [ 12 | "ckotzbauer" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: create-release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version" 8 | required: true 9 | 10 | jobs: 11 | release: 12 | uses: ckotzbauer/actions-toolkit/.github/workflows/toolkit-release-goreleaser.yml@0.46.0 13 | with: 14 | version: ${{ github.event.inputs.version }} 15 | docker-platforms: linux/amd64,linux/arm64 16 | docker-tags: | 17 | ckotzbauer/access-manager:${{ github.event.inputs.version }} 18 | ckotzbauer/access-manager:latest 19 | ghcr.io/ckotzbauer/access-manager:${{ github.event.inputs.version }} 20 | ghcr.io/ckotzbauer/access-manager:latest 21 | cosign-repository: ghcr.io/ckotzbauer/access-manager-metadata 22 | secrets: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | pat: ${{ secrets.REPO_ACCESS }} 25 | dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }} 26 | dockerhub-password: ${{ secrets.DOCKERHUB_PASSWORD }} 27 | ghcr-password: ${{ secrets.GHCR_PASSWORD }} 28 | -------------------------------------------------------------------------------- /.github/workflows/label-issues.yml: -------------------------------------------------------------------------------- 1 | name: label-issues 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | - edited 7 | pull_request: 8 | types: 9 | - opened 10 | issues: 11 | types: 12 | - opened 13 | 14 | jobs: 15 | label-issues: 16 | uses: ckotzbauer/actions-toolkit/.github/workflows/toolkit-label-issues.yml@0.46.0 17 | secrets: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/size-label.yml: -------------------------------------------------------------------------------- 1 | name: size-label 2 | on: 3 | pull_request_target: 4 | types: [opened, reopened, synchronize] 5 | 6 | jobs: 7 | size-label: 8 | uses: ckotzbauer/actions-toolkit/.github/workflows/toolkit-size-label.yml@0.46.0 9 | secrets: 10 | token: ${{ secrets.GITHUB_TOKEN }} 11 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: stale 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | stale: 8 | uses: ckotzbauer/actions-toolkit/.github/workflows/toolkit-stale.yml@0.46.0 9 | secrets: 10 | token: ${{ secrets.GITHUB_TOKEN }} 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | 8 | jobs: 9 | e2e-test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | kubernetes-version: 15 | - "1.27.11" 16 | - "1.28.7" 17 | - "1.29.2" 18 | steps: 19 | - name: Setup Go 20 | uses: ckotzbauer/actions-toolkit/setup-go@0.46.0 21 | 22 | - name: Set up QEMU 23 | id: qemu 24 | uses: docker/setup-qemu-action@v3 25 | with: 26 | image: tonistiigi/binfmt:latest 27 | platforms: all 28 | 29 | - name: Set up Docker Buildx 30 | id: buildx 31 | uses: docker/setup-buildx-action@v3 32 | 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - uses: azure/setup-kubectl@v3.2 37 | with: 38 | version: "v${{ matrix.kubernetes-version }}" 39 | 40 | - name: Install GoReleaser 41 | uses: goreleaser/goreleaser-action@v6 42 | with: 43 | version: "v2.0.1" 44 | install-only: true 45 | 46 | - name: Build binary 47 | run: make build 48 | 49 | - name: Build image 50 | uses: docker/build-push-action@v6 51 | with: 52 | context: . 53 | push: false 54 | load: true 55 | platforms: linux/amd64 56 | tags: | 57 | ckotzbauer/access-manager:latest 58 | 59 | - name: Execute Tests 60 | run: make e2e-test -e K8S_VERSION=${{ matrix.kubernetes-version }} 61 | 62 | build: 63 | uses: ckotzbauer/actions-toolkit/.github/workflows/toolkit-build-test.yml@0.46.0 64 | with: 65 | install-go: true 66 | install-goreleaser: true 67 | install-kubebuilder: true 68 | build-commands: make build 69 | test-commands: make test 70 | build-image: true 71 | docker-tag: ghcr.io/ckotzbauer/access-manager:latest 72 | scan-image: true 73 | -------------------------------------------------------------------------------- /.github/workflows/update-snyk.yml: -------------------------------------------------------------------------------- 1 | name: update-snyk 2 | 3 | on: 4 | schedule: 5 | - cron: "0 12 * * 1" 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | update-snyk: 10 | uses: ckotzbauer/actions-toolkit/.github/workflows/toolkit-scan-snyk.yml@0.46.0 11 | with: 12 | install-go: true 13 | scan-commands: snyk monitor 14 | secrets: 15 | token: ${{ secrets.SNYK_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | dist 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | e2e/kind-kubeconfig 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 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: access-manager 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | goarm: 11 | - "6" 12 | - "7" 13 | goarch: 14 | - "amd64" 15 | - "arm64" 16 | ignore: 17 | - goos: linux 18 | goarch: "386" 19 | ldflags: 20 | - -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.Date={{.CommitDate}} -X main.BuiltBy=goreleaser 21 | mod_timestamp: "{{.CommitTimestamp}}" 22 | flags: 23 | - -trimpath 24 | 25 | sboms: 26 | - artifacts: archive 27 | args: ["$artifact", "--file", "$document", "--output", "json"] 28 | 29 | snapshot: 30 | name_template: "{{ .Version }}" 31 | 32 | release: 33 | disable: true 34 | 35 | changelog: 36 | disable: true 37 | 38 | source: 39 | enabled: true 40 | 41 | signs: 42 | - cmd: cosign 43 | certificate: '${artifact}.pem' 44 | args: 45 | - sign-blob 46 | - '-y' 47 | - '--output-certificate=${certificate}' 48 | - '--output-signature=${signature}' 49 | - '${artifact}' 50 | artifacts: all 51 | output: true 52 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Verwendet IntelliSense zum Ermitteln möglicher Attribute. 3 | // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. 4 | // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Start Controller", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/main.go", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d as alpine 2 | 3 | ARG TARGETARCH 4 | 5 | RUN set -eux; \ 6 | apk add -U --no-cache ca-certificates 7 | 8 | 9 | FROM scratch 10 | 11 | ARG TARGETOS 12 | ARG TARGETARCH 13 | 14 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 15 | COPY dist/access-manager_${TARGETOS}_${TARGETARCH}*/access-manager /usr/local/bin/access-manager 16 | 17 | ENTRYPOINT ["/usr/local/bin/access-manager"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Christian Kotzbauer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Current Operator version 2 | ifeq (,${VERSION}) 3 | BIN_VERSION=latest 4 | else 5 | BIN_VERSION=${VERSION} 6 | endif 7 | 8 | # Image URL to use all building/pushing image targets 9 | IMG ?= ckotzbauer/access-manager 10 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 11 | CRD_OPTIONS ?= "crd" 12 | 13 | # default k8s version for e2e tests 14 | K8S_VERSION ?= 1.29.2 15 | 16 | TARGETOS=linux 17 | ifeq (,${TARGETARCH}) 18 | TARGETARCH=$(shell go env GOARCH) 19 | endif 20 | 21 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 22 | ifeq (,$(shell go env GOBIN)) 23 | GOBIN=$(shell go env GOPATH)/bin 24 | else 25 | GOBIN=$(shell go env GOBIN) 26 | endif 27 | 28 | all: build 29 | 30 | # Run unit-tests 31 | test: generate fmt vet manifests 32 | go test github.com/ckotzbauer/access-manager/pkg/reconciler -coverprofile cover.out 33 | 34 | # Run e2e-tests 35 | e2e-test: kind 36 | cd e2e && \ 37 | bash test.sh $(KIND) $(K8S_VERSION) 38 | 39 | build: generate fmt vet 40 | goreleaser build --clean --single-target --snapshot 41 | 42 | # Run against the configured Kubernetes cluster in ~/.kube/config 43 | run: generate fmt vet manifests 44 | go run ./main.go 45 | 46 | # Install CRDs into a cluster 47 | install: manifests 48 | kubectl apply -f config/crd 49 | 50 | # Uninstall CRDs from a cluster 51 | uninstall: manifests 52 | kubectl delete -f config/rbac 53 | kubectl delete -f config/manager 54 | kubectl delete -f config/crd 55 | 56 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 57 | deploy: manifests 58 | kubectl apply -f config/rbac 59 | kubectl apply -f config/manager 60 | 61 | # Generate manifests e.g. CRD, RBAC etc. 62 | manifests: controller-gen 63 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd 64 | 65 | # Run go fmt against code 66 | fmt: 67 | go fmt ./... 68 | 69 | # Run go vet against code 70 | vet: 71 | go vet ./... 72 | 73 | # Generate code 74 | generate: controller-gen 75 | $(CONTROLLER_GEN) object:headerFile= paths="./..." 76 | 77 | # find or download controller-gen 78 | # download controller-gen if necessary 79 | controller-gen: 80 | ifeq (, $(shell which controller-gen)) 81 | @{ \ 82 | set -e ;\ 83 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 84 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 85 | go mod init tmp ;\ 86 | go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0 ;\ 87 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 88 | } 89 | CONTROLLER_GEN=$(GOBIN)/controller-gen 90 | else 91 | CONTROLLER_GEN=$(shell which controller-gen) 92 | endif 93 | 94 | # find or download kind 95 | # download kind if necessary 96 | kind: 97 | ifeq (, $(shell which kind)) 98 | @{ \ 99 | set -e ;\ 100 | KIND_TMP_DIR=$$(mktemp -d) ;\ 101 | cd $$KIND_TMP_DIR ;\ 102 | go mod init tmp ;\ 103 | go download sigs.k8s.io/kind@v0.22.0 ;\ 104 | rm -rf $$KIND_TMP_DIR ;\ 105 | } 106 | KIND=$(GOBIN)/kind 107 | else 108 | KIND=$(shell which kind) 109 | endif 110 | 111 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: access-manager.io 2 | layout: go.kubebuilder.io/v3 3 | multigroup: true 4 | projectName: access-manager 5 | repo: github.com/ckotzbauer/access-manager 6 | resources: 7 | - kind: RbacDefinition 8 | version: v1beta1 9 | - group: access-manager.io 10 | kind: SyncSecretDefinition 11 | version: v1beta1 12 | version: 3-alpha 13 | plugins: 14 | go.sdk.operatorframework.io/v2-alpha: {} 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # access-manager 2 | 3 | ![test](https://github.com/ckotzbauer/access-manager/workflows/test/badge.svg) 4 | 5 | The Access-Manager is a Kubernetes-Operator using the [Operator-SDK](https://github.com/operator-framework/operator-sdk) to simplify complex [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) configurations in your cluster and spread secrets across namespaces. 6 | 7 | ## Motivation 8 | 9 | The idea for this came up, when managing many different RBAC-Roles on namespace-basis. This was getting more complex over time, and the administrator always has to ensure that the correct roles are applied for different people or ServiceAccounts in multiple namespaces. The scope of the operator is limited to the creation and removal of `RoleBinding`s and `ClusterRoleBinding`s. So all referenced `Role`s and `ClusterRole`s have to exist. Let's automate it. 10 | 11 | ## Kubernetes Compatibility 12 | 13 | The image contains versions of `k8s.io/client-go`. Kubernetes aims to provide forwards & backwards compatibility of one minor version between client and server: 14 | 15 | | access-manager | k8s.io/client-go | k8s.io/apimachinery | expected kubernetes compatibility | 16 | |-----------------|------------------|---------------------|-----------------------------------| 17 | | main | v0.28.1 | v0.28.1 | 1.27.x, 1.28.x, 1.29.x | 18 | | 0.12.x | v0.28.1 | v0.28.1 | 1.27.x, 1.28.x, 1.29.x | 19 | | 0.11.x | v0.26.0 | v0.26.0 | 1.25.x, 1.26.x, 1.27.x | 20 | | 0.10.x | v0.24.3 | v0.24.3 | 1.23.x, 1.24.x, 1.25.x | 21 | | 0.9.x | v0.23.5 | v0.23.5 | 1.22.x, 1.23.x, 1.24.x | 22 | | 0.8.x | v0.23.0 | v0.23.0 | 1.22.x, 1.23.x, 1.24.x | 23 | | 0.7.x | v0.22.1 | v0.22.1 | 1.21.x, 1.22.x, 1.23.x | 24 | | 0.6.x | v0.21.1 | v0.21.1 | 1.20.x, 1.21.x, 1.22.x | 25 | | 0.5.x | v0.20.1 | v0.20.1 | 1.19.x, 1.20.x, 1.21.x | 26 | | 0.4.x | v0.19.2 | v0.19.2 | 1.18.x, 1.19.x, 1.20.x | 27 | | 0.3.x | v0.18.8 | v0.18.8 | 1.17.x, 1.18.x, 1.19.x | 28 | | 0.2.x | v12.0.0 | v0.18.5 | 1.17.x, 1.18.x, 1.19.x | 29 | | 0.1.x | v12.0.0 | v0.18.3 | 1.17.x, 1.18.x, 1.19.x | 30 | 31 | See the [release notes](https://github.com/ckotzbauer/access-manager/releases) for specific version compatibility information, including which 32 | combination have been formally tested. 33 | 34 | ## Installation 35 | 36 | **Note:** The `ServiceAccount` must have at least the permissions that it should grant. The `cluster-admin` `ClusterRole` is assigned to the `ServiceAccount` by default. 37 | 38 | #### Manifests 39 | 40 | ``` 41 | kubectl apply -f config/crd/access-manager.io_rbacdefinitions.yaml 42 | kubectl apply -f config/crd/access-manager.io_syncsecretdefinitions.yaml 43 | kubectl apply -f config/rbac 44 | kubectl apply -f config/manager 45 | ``` 46 | 47 | #### Helm-Chart 48 | 49 | ``` 50 | helm repo add ckotzbauer https://ckotzbauer.github.io/helm-charts 51 | helm install ckotzbauer/access-manager 52 | ``` 53 | 54 | ## Examples 55 | 56 | ### RBAC-Definition 57 | 58 | The `RbacDefinition` itself is cluster-scoped. 59 | 60 | ```yaml 61 | apiVersion: access-manager.io/v1beta1 62 | kind: RbacDefinition 63 | metadata: 64 | name: example-definition 65 | spec: 66 | namespaced: 67 | - namespace: 68 | name: my-product 69 | bindings: 70 | - roleName: my-product-management 71 | kind: Role 72 | subjects: 73 | - name: my-product-team 74 | kind: Group 75 | - name: devops-team 76 | kind: Group 77 | - namespaceSelector: 78 | matchLabels: 79 | ci: "true" 80 | bindings: 81 | - roleName: ci-deploy 82 | kind: ClusterRole 83 | subjects: 84 | - name: ci 85 | namespace: ci-service 86 | kind: ServiceAccount 87 | cluster: 88 | - name: john-view-binding 89 | clusterRoleName: view 90 | subjects: 91 | - name: john 92 | kind: User 93 | ``` 94 | 95 | This would create the following objects: 96 | - A `RoleBinding` named `my-product-management` in the namespace `my-product` assigning the `my-product-management` `Role` to the `Group`s `my-product-team` and `devops-team`. 97 | - A `RoleBinding` named `ci-deploy` in each namespace labeled with `ci: true` assigning the `ci-deploy` `ClusterRole` to the `ServiceAccount` `ci` in the `ci-service` namespace. 98 | - A `ClusterRoleBinding` named `john-view-binding` assigning the `view` `ClusterRole` to the `User` `john`. 99 | 100 | For more details, please read the [api-docs](https://github.com/ckotzbauer/access-manager/blob/master/docs/api.md) and view YAMLs in the `examples` directory. 101 | 102 | 103 | ### Behaviors 104 | 105 | - A `RbacDefinition` can be marked as "paused" (set `spec.paused` to `true`), so that the operator will not interfere you. 106 | - The `RoleBinding`s and `ClusterRoleBinding`s are named the same as the given `Role` or `ClusterRole` unless the name is explicitly specified. 107 | - If there is a existing binding with the same name that is not owned by the `RbacDefinition` it is not touched. 108 | - The operator detects changes to all `RbacDefinition`s, `Namespace`s and `ServiceAccount`s automatically. 109 | 110 | 111 | ### SyncSecret-Definition 112 | 113 | The `SyncSecretDefinition` itself is cluster-scoped. 114 | 115 | ```yaml 116 | apiVersion: access-manager.io/v1beta1 117 | kind: SyncSecretDefinition 118 | metadata: 119 | name: example-definition 120 | spec: 121 | source: 122 | name: source-secret 123 | namespace: default 124 | targets: 125 | - namespace: 126 | name: my-product 127 | - namespaceSelector: 128 | matchLabels: 129 | ci: "true" 130 | ``` 131 | 132 | This would create the following secret: 133 | - A `Secret` named `source-secret` in the namespace `my-product` and each namespace labeled with `ci: true`. 134 | 135 | For more details, please read the [api-docs](https://github.com/ckotzbauer/access-manager/blob/master/docs/api.md) and view YAMLs in the `examples` directory. 136 | 137 | 138 | ### Behaviors 139 | 140 | - A `SyncSecretDefinition` can be marked as "paused" (set `spec.paused` to `true`), so that the operator will not interfere you. 141 | - The `Secrets`s are named the same as the given `Secret` in "source". 142 | - If there is a existing secret with the same name that is not owned by the `SyncSecretDefinition` it is not touched. 143 | - The operator detects changes to all `SyncSecretDefinition`s, `Namespace`s and source `Secrets`s automatically. 144 | 145 | 146 | ## Roadmap 147 | 148 | - Expose Prometheus metrics about created bindings and reconcile errors. 149 | 150 | 151 | #### Credits 152 | 153 | This projects was inspired by the [RBACManager](https://github.com/FairwindsOps/rbac-manager). 154 | 155 | [License](https://github.com/ckotzbauer/access-manager/blob/master/LICENSE) 156 | -------- 157 | [Changelog](https://github.com/ckotzbauer/access-manager/blob/master/CHANGELOG.md) 158 | -------- 159 | 160 | ## Contributing 161 | 162 | Please refer to the [Contribution guildelines](https://github.com/ckotzbauer/.github/blob/main/CONTRIBUTING.md). 163 | 164 | ## Code of conduct 165 | 166 | Please refer to the [Conduct guildelines](https://github.com/ckotzbauer/.github/blob/main/CODE_OF_CONDUCT.md). 167 | 168 | ## Security 169 | 170 | Please refer to the [Security process](https://github.com/ckotzbauer/.github/blob/main/SECURITY.md). 171 | 172 | -------------------------------------------------------------------------------- /apis/access-manager.io/v1beta1/common_types.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | type NamespaceSpec struct { 4 | Name string `json:"name"` 5 | } 6 | -------------------------------------------------------------------------------- /apis/access-manager.io/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1beta1 contains API Schema definitions for the rbacdefinitions v1beta1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=access-manager.io 20 | package v1beta1 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: "access-manager.io", Version: "v1beta1"} 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 | -------------------------------------------------------------------------------- /apis/access-manager.io/v1beta1/rbacdefinition_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1beta1 18 | 19 | import ( 20 | rbacv1 "k8s.io/api/rbac/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | type BindingsSpec struct { 25 | // +kubebuilder:default="" 26 | // +kubebuilder:validation:Optional 27 | Name string `json:"name"` 28 | RoleName string `json:"roleName"` 29 | Kind string `json:"kind"` 30 | // +kubebuilder:validation:Optional 31 | Subjects []rbacv1.Subject `json:"subjects"` 32 | // +kubebuilder:default=false 33 | // +kubebuilder:validation:Optional 34 | AllServiceAccounts bool `json:"allServiceAccounts"` 35 | } 36 | 37 | type NamespacedSpec struct { 38 | Namespace NamespaceSpec `json:"namespace,omitempty"` 39 | NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"` 40 | Bindings []BindingsSpec `json:"bindings"` 41 | } 42 | 43 | type ClusterSpec struct { 44 | Name string `json:"name"` 45 | ClusterRoleName string `json:"clusterRoleName"` 46 | Subjects []rbacv1.Subject `json:"subjects"` 47 | } 48 | 49 | // RbacDefinitionSpec defines the desired state of RbacDefinition 50 | type RbacDefinitionSpec struct { 51 | Paused bool `json:"paused,omitempty"` 52 | Namespaced []NamespacedSpec `json:"namespaced,omitempty"` 53 | Cluster []ClusterSpec `json:"cluster,omitempty"` 54 | } 55 | 56 | // RbacDefinitionStatus defines the observed state of RbacDefinition 57 | type RbacDefinitionStatus struct { 58 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 59 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 60 | // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html 61 | } 62 | 63 | // +kubebuilder:object:root=true 64 | 65 | // RbacDefinition is the Schema for the rbacdefinitions API 66 | // +kubebuilder:subresource:status 67 | // +kubebuilder:resource:path=rbacdefinitions,scope=Cluster 68 | type RbacDefinition struct { 69 | metav1.TypeMeta `json:",inline"` 70 | metav1.ObjectMeta `json:"metadata,omitempty"` 71 | 72 | Spec RbacDefinitionSpec `json:"spec,omitempty"` 73 | Status RbacDefinitionStatus `json:"status,omitempty"` 74 | } 75 | 76 | // +kubebuilder:object:root=true 77 | 78 | // RbacDefinitionList contains a list of RbacDefinition 79 | type RbacDefinitionList struct { 80 | metav1.TypeMeta `json:",inline"` 81 | metav1.ListMeta `json:"metadata,omitempty"` 82 | Items []RbacDefinition `json:"items"` 83 | } 84 | 85 | func init() { 86 | SchemeBuilder.Register(&RbacDefinition{}, &RbacDefinitionList{}) 87 | } 88 | -------------------------------------------------------------------------------- /apis/access-manager.io/v1beta1/syncsecretdefinition_types.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // SyncSecretDefinitionSpec defines the desired state of SyncSecretDefinition 8 | type SyncSecretDefinitionSpec struct { 9 | // +kubebuilder:default=false 10 | // +kubebuilder:validation:Optional 11 | Paused bool `json:"paused,omitempty"` 12 | Source SourceSpec `json:"source"` 13 | Targets []TargetSpec `json:"targets"` 14 | } 15 | 16 | type TargetSpec struct { 17 | Namespace NamespaceSpec `json:"namespace,omitempty"` 18 | NamespaceSelector metav1.LabelSelector `json:"namespaceSelector,omitempty"` 19 | } 20 | 21 | type SourceSpec struct { 22 | Namespace string `json:"namespace"` 23 | Name string `json:"name"` 24 | } 25 | 26 | // SyncSecretDefinitionStatus defines the observed state of SyncSecretDefinition 27 | type SyncSecretDefinitionStatus struct { 28 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 29 | // Important: Run "make" to regenerate code after modifying this file 30 | } 31 | 32 | // +kubebuilder:object:root=true 33 | // +kubebuilder:subresource:status 34 | // +kubebuilder:resource:scope=Cluster 35 | 36 | // SyncSecretDefinition is the Schema for the syncsecretdefinitions API 37 | type SyncSecretDefinition struct { 38 | metav1.TypeMeta `json:",inline"` 39 | metav1.ObjectMeta `json:"metadata,omitempty"` 40 | 41 | Spec SyncSecretDefinitionSpec `json:"spec,omitempty"` 42 | Status SyncSecretDefinitionStatus `json:"status,omitempty"` 43 | } 44 | 45 | // +kubebuilder:object:root=true 46 | 47 | // SyncSecretDefinitionList contains a list of SyncSecretDefinition 48 | type SyncSecretDefinitionList struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ListMeta `json:"metadata,omitempty"` 51 | Items []SyncSecretDefinition `json:"items"` 52 | } 53 | 54 | func init() { 55 | SchemeBuilder.Register(&SyncSecretDefinition{}, &SyncSecretDefinitionList{}) 56 | } 57 | -------------------------------------------------------------------------------- /apis/access-manager.io/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // Code generated by controller-gen. DO NOT EDIT. 4 | 5 | package v1beta1 6 | 7 | import ( 8 | "k8s.io/api/rbac/v1" 9 | runtime "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 13 | func (in *BindingsSpec) DeepCopyInto(out *BindingsSpec) { 14 | *out = *in 15 | if in.Subjects != nil { 16 | in, out := &in.Subjects, &out.Subjects 17 | *out = make([]v1.Subject, len(*in)) 18 | copy(*out, *in) 19 | } 20 | } 21 | 22 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindingsSpec. 23 | func (in *BindingsSpec) DeepCopy() *BindingsSpec { 24 | if in == nil { 25 | return nil 26 | } 27 | out := new(BindingsSpec) 28 | in.DeepCopyInto(out) 29 | return out 30 | } 31 | 32 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 33 | func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { 34 | *out = *in 35 | if in.Subjects != nil { 36 | in, out := &in.Subjects, &out.Subjects 37 | *out = make([]v1.Subject, len(*in)) 38 | copy(*out, *in) 39 | } 40 | } 41 | 42 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. 43 | func (in *ClusterSpec) DeepCopy() *ClusterSpec { 44 | if in == nil { 45 | return nil 46 | } 47 | out := new(ClusterSpec) 48 | in.DeepCopyInto(out) 49 | return out 50 | } 51 | 52 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 53 | func (in *NamespaceSpec) DeepCopyInto(out *NamespaceSpec) { 54 | *out = *in 55 | } 56 | 57 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSpec. 58 | func (in *NamespaceSpec) DeepCopy() *NamespaceSpec { 59 | if in == nil { 60 | return nil 61 | } 62 | out := new(NamespaceSpec) 63 | in.DeepCopyInto(out) 64 | return out 65 | } 66 | 67 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 68 | func (in *NamespacedSpec) DeepCopyInto(out *NamespacedSpec) { 69 | *out = *in 70 | out.Namespace = in.Namespace 71 | in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) 72 | if in.Bindings != nil { 73 | in, out := &in.Bindings, &out.Bindings 74 | *out = make([]BindingsSpec, len(*in)) 75 | for i := range *in { 76 | (*in)[i].DeepCopyInto(&(*out)[i]) 77 | } 78 | } 79 | } 80 | 81 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedSpec. 82 | func (in *NamespacedSpec) DeepCopy() *NamespacedSpec { 83 | if in == nil { 84 | return nil 85 | } 86 | out := new(NamespacedSpec) 87 | in.DeepCopyInto(out) 88 | return out 89 | } 90 | 91 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 92 | func (in *RbacDefinition) DeepCopyInto(out *RbacDefinition) { 93 | *out = *in 94 | out.TypeMeta = in.TypeMeta 95 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 96 | in.Spec.DeepCopyInto(&out.Spec) 97 | out.Status = in.Status 98 | } 99 | 100 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RbacDefinition. 101 | func (in *RbacDefinition) DeepCopy() *RbacDefinition { 102 | if in == nil { 103 | return nil 104 | } 105 | out := new(RbacDefinition) 106 | in.DeepCopyInto(out) 107 | return out 108 | } 109 | 110 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 111 | func (in *RbacDefinition) DeepCopyObject() runtime.Object { 112 | if c := in.DeepCopy(); c != nil { 113 | return c 114 | } 115 | return nil 116 | } 117 | 118 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 119 | func (in *RbacDefinitionList) DeepCopyInto(out *RbacDefinitionList) { 120 | *out = *in 121 | out.TypeMeta = in.TypeMeta 122 | in.ListMeta.DeepCopyInto(&out.ListMeta) 123 | if in.Items != nil { 124 | in, out := &in.Items, &out.Items 125 | *out = make([]RbacDefinition, len(*in)) 126 | for i := range *in { 127 | (*in)[i].DeepCopyInto(&(*out)[i]) 128 | } 129 | } 130 | } 131 | 132 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RbacDefinitionList. 133 | func (in *RbacDefinitionList) DeepCopy() *RbacDefinitionList { 134 | if in == nil { 135 | return nil 136 | } 137 | out := new(RbacDefinitionList) 138 | in.DeepCopyInto(out) 139 | return out 140 | } 141 | 142 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 143 | func (in *RbacDefinitionList) DeepCopyObject() runtime.Object { 144 | if c := in.DeepCopy(); c != nil { 145 | return c 146 | } 147 | return nil 148 | } 149 | 150 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 151 | func (in *RbacDefinitionSpec) DeepCopyInto(out *RbacDefinitionSpec) { 152 | *out = *in 153 | if in.Namespaced != nil { 154 | in, out := &in.Namespaced, &out.Namespaced 155 | *out = make([]NamespacedSpec, len(*in)) 156 | for i := range *in { 157 | (*in)[i].DeepCopyInto(&(*out)[i]) 158 | } 159 | } 160 | if in.Cluster != nil { 161 | in, out := &in.Cluster, &out.Cluster 162 | *out = make([]ClusterSpec, len(*in)) 163 | for i := range *in { 164 | (*in)[i].DeepCopyInto(&(*out)[i]) 165 | } 166 | } 167 | } 168 | 169 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RbacDefinitionSpec. 170 | func (in *RbacDefinitionSpec) DeepCopy() *RbacDefinitionSpec { 171 | if in == nil { 172 | return nil 173 | } 174 | out := new(RbacDefinitionSpec) 175 | in.DeepCopyInto(out) 176 | return out 177 | } 178 | 179 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 180 | func (in *RbacDefinitionStatus) DeepCopyInto(out *RbacDefinitionStatus) { 181 | *out = *in 182 | } 183 | 184 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RbacDefinitionStatus. 185 | func (in *RbacDefinitionStatus) DeepCopy() *RbacDefinitionStatus { 186 | if in == nil { 187 | return nil 188 | } 189 | out := new(RbacDefinitionStatus) 190 | in.DeepCopyInto(out) 191 | return out 192 | } 193 | 194 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 195 | func (in *SourceSpec) DeepCopyInto(out *SourceSpec) { 196 | *out = *in 197 | } 198 | 199 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceSpec. 200 | func (in *SourceSpec) DeepCopy() *SourceSpec { 201 | if in == nil { 202 | return nil 203 | } 204 | out := new(SourceSpec) 205 | in.DeepCopyInto(out) 206 | return out 207 | } 208 | 209 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 210 | func (in *SyncSecretDefinition) DeepCopyInto(out *SyncSecretDefinition) { 211 | *out = *in 212 | out.TypeMeta = in.TypeMeta 213 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 214 | in.Spec.DeepCopyInto(&out.Spec) 215 | out.Status = in.Status 216 | } 217 | 218 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncSecretDefinition. 219 | func (in *SyncSecretDefinition) DeepCopy() *SyncSecretDefinition { 220 | if in == nil { 221 | return nil 222 | } 223 | out := new(SyncSecretDefinition) 224 | in.DeepCopyInto(out) 225 | return out 226 | } 227 | 228 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 229 | func (in *SyncSecretDefinition) DeepCopyObject() runtime.Object { 230 | if c := in.DeepCopy(); c != nil { 231 | return c 232 | } 233 | return nil 234 | } 235 | 236 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 237 | func (in *SyncSecretDefinitionList) DeepCopyInto(out *SyncSecretDefinitionList) { 238 | *out = *in 239 | out.TypeMeta = in.TypeMeta 240 | in.ListMeta.DeepCopyInto(&out.ListMeta) 241 | if in.Items != nil { 242 | in, out := &in.Items, &out.Items 243 | *out = make([]SyncSecretDefinition, len(*in)) 244 | for i := range *in { 245 | (*in)[i].DeepCopyInto(&(*out)[i]) 246 | } 247 | } 248 | } 249 | 250 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncSecretDefinitionList. 251 | func (in *SyncSecretDefinitionList) DeepCopy() *SyncSecretDefinitionList { 252 | if in == nil { 253 | return nil 254 | } 255 | out := new(SyncSecretDefinitionList) 256 | in.DeepCopyInto(out) 257 | return out 258 | } 259 | 260 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 261 | func (in *SyncSecretDefinitionList) DeepCopyObject() runtime.Object { 262 | if c := in.DeepCopy(); c != nil { 263 | return c 264 | } 265 | return nil 266 | } 267 | 268 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 269 | func (in *SyncSecretDefinitionSpec) DeepCopyInto(out *SyncSecretDefinitionSpec) { 270 | *out = *in 271 | out.Source = in.Source 272 | if in.Targets != nil { 273 | in, out := &in.Targets, &out.Targets 274 | *out = make([]TargetSpec, len(*in)) 275 | for i := range *in { 276 | (*in)[i].DeepCopyInto(&(*out)[i]) 277 | } 278 | } 279 | } 280 | 281 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncSecretDefinitionSpec. 282 | func (in *SyncSecretDefinitionSpec) DeepCopy() *SyncSecretDefinitionSpec { 283 | if in == nil { 284 | return nil 285 | } 286 | out := new(SyncSecretDefinitionSpec) 287 | in.DeepCopyInto(out) 288 | return out 289 | } 290 | 291 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 292 | func (in *SyncSecretDefinitionStatus) DeepCopyInto(out *SyncSecretDefinitionStatus) { 293 | *out = *in 294 | } 295 | 296 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncSecretDefinitionStatus. 297 | func (in *SyncSecretDefinitionStatus) DeepCopy() *SyncSecretDefinitionStatus { 298 | if in == nil { 299 | return nil 300 | } 301 | out := new(SyncSecretDefinitionStatus) 302 | in.DeepCopyInto(out) 303 | return out 304 | } 305 | 306 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 307 | func (in *TargetSpec) DeepCopyInto(out *TargetSpec) { 308 | *out = *in 309 | out.Namespace = in.Namespace 310 | in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) 311 | } 312 | 313 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSpec. 314 | func (in *TargetSpec) DeepCopy() *TargetSpec { 315 | if in == nil { 316 | return nil 317 | } 318 | out := new(TargetSpec) 319 | in.DeepCopyInto(out) 320 | return out 321 | } 322 | -------------------------------------------------------------------------------- /config/crd/access-manager.io_rbacdefinitions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.13.0 7 | name: rbacdefinitions.access-manager.io 8 | spec: 9 | group: access-manager.io 10 | names: 11 | kind: RbacDefinition 12 | listKind: RbacDefinitionList 13 | plural: rbacdefinitions 14 | singular: rbacdefinition 15 | scope: Cluster 16 | versions: 17 | - name: v1beta1 18 | schema: 19 | openAPIV3Schema: 20 | description: RbacDefinition is the Schema for the rbacdefinitions API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: RbacDefinitionSpec defines the desired state of RbacDefinition 36 | properties: 37 | cluster: 38 | items: 39 | properties: 40 | clusterRoleName: 41 | type: string 42 | name: 43 | type: string 44 | subjects: 45 | items: 46 | description: Subject contains a reference to the object or 47 | user identities a role binding applies to. This can either 48 | hold a direct API object reference, or a value for non-objects 49 | such as user and group names. 50 | properties: 51 | apiGroup: 52 | description: APIGroup holds the API group of the referenced 53 | subject. Defaults to "" for ServiceAccount subjects. 54 | Defaults to "rbac.authorization.k8s.io" for User and 55 | Group subjects. 56 | type: string 57 | kind: 58 | description: Kind of object being referenced. Values defined 59 | by this API group are "User", "Group", and "ServiceAccount". 60 | If the Authorizer does not recognized the kind value, 61 | the Authorizer should report an error. 62 | type: string 63 | name: 64 | description: Name of the object being referenced. 65 | type: string 66 | namespace: 67 | description: Namespace of the referenced object. If the 68 | object kind is non-namespace, such as "User" or "Group", 69 | and this value is not empty the Authorizer should report 70 | an error. 71 | type: string 72 | required: 73 | - kind 74 | - name 75 | type: object 76 | x-kubernetes-map-type: atomic 77 | type: array 78 | required: 79 | - clusterRoleName 80 | - name 81 | - subjects 82 | type: object 83 | type: array 84 | namespaced: 85 | items: 86 | properties: 87 | bindings: 88 | items: 89 | properties: 90 | allServiceAccounts: 91 | default: false 92 | type: boolean 93 | kind: 94 | type: string 95 | name: 96 | default: "" 97 | type: string 98 | roleName: 99 | type: string 100 | subjects: 101 | items: 102 | description: Subject contains a reference to the object 103 | or user identities a role binding applies to. This 104 | can either hold a direct API object reference, or 105 | a value for non-objects such as user and group names. 106 | properties: 107 | apiGroup: 108 | description: APIGroup holds the API group of the 109 | referenced subject. Defaults to "" for ServiceAccount 110 | subjects. Defaults to "rbac.authorization.k8s.io" 111 | for User and Group subjects. 112 | type: string 113 | kind: 114 | description: Kind of object being referenced. Values 115 | defined by this API group are "User", "Group", 116 | and "ServiceAccount". If the Authorizer does not 117 | recognized the kind value, the Authorizer should 118 | report an error. 119 | type: string 120 | name: 121 | description: Name of the object being referenced. 122 | type: string 123 | namespace: 124 | description: Namespace of the referenced object. If 125 | the object kind is non-namespace, such as "User" 126 | or "Group", and this value is not empty the Authorizer 127 | should report an error. 128 | type: string 129 | required: 130 | - kind 131 | - name 132 | type: object 133 | x-kubernetes-map-type: atomic 134 | type: array 135 | required: 136 | - kind 137 | - roleName 138 | type: object 139 | type: array 140 | namespace: 141 | properties: 142 | name: 143 | type: string 144 | required: 145 | - name 146 | type: object 147 | namespaceSelector: 148 | description: A label selector is a label query over a set of 149 | resources. The result of matchLabels and matchExpressions 150 | are ANDed. An empty label selector matches all objects. A 151 | null label selector matches no objects. 152 | properties: 153 | matchExpressions: 154 | description: matchExpressions is a list of label selector 155 | requirements. The requirements are ANDed. 156 | items: 157 | description: A label selector requirement is a selector 158 | that contains values, a key, and an operator that relates 159 | the key and values. 160 | properties: 161 | key: 162 | description: key is the label key that the selector 163 | applies to. 164 | type: string 165 | operator: 166 | description: operator represents a key's relationship 167 | to a set of values. Valid operators are In, NotIn, 168 | Exists and DoesNotExist. 169 | type: string 170 | values: 171 | description: values is an array of string values. 172 | If the operator is In or NotIn, the values array 173 | must be non-empty. If the operator is Exists or 174 | DoesNotExist, the values array must be empty. This 175 | array is replaced during a strategic merge patch. 176 | items: 177 | type: string 178 | type: array 179 | required: 180 | - key 181 | - operator 182 | type: object 183 | type: array 184 | matchLabels: 185 | additionalProperties: 186 | type: string 187 | description: matchLabels is a map of {key,value} pairs. 188 | A single {key,value} in the matchLabels map is equivalent 189 | to an element of matchExpressions, whose key field is 190 | "key", the operator is "In", and the values array contains 191 | only "value". The requirements are ANDed. 192 | type: object 193 | type: object 194 | x-kubernetes-map-type: atomic 195 | required: 196 | - bindings 197 | type: object 198 | type: array 199 | paused: 200 | type: boolean 201 | type: object 202 | status: 203 | description: RbacDefinitionStatus defines the observed state of RbacDefinition 204 | type: object 205 | type: object 206 | served: true 207 | storage: true 208 | subresources: 209 | status: {} 210 | -------------------------------------------------------------------------------- /config/crd/access-manager.io_syncsecretdefinitions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.13.0 7 | name: syncsecretdefinitions.access-manager.io 8 | spec: 9 | group: access-manager.io 10 | names: 11 | kind: SyncSecretDefinition 12 | listKind: SyncSecretDefinitionList 13 | plural: syncsecretdefinitions 14 | singular: syncsecretdefinition 15 | scope: Cluster 16 | versions: 17 | - name: v1beta1 18 | schema: 19 | openAPIV3Schema: 20 | description: SyncSecretDefinition is the Schema for the syncsecretdefinitions 21 | API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: SyncSecretDefinitionSpec defines the desired state of SyncSecretDefinition 37 | properties: 38 | paused: 39 | default: false 40 | type: boolean 41 | source: 42 | properties: 43 | name: 44 | type: string 45 | namespace: 46 | type: string 47 | required: 48 | - name 49 | - namespace 50 | type: object 51 | targets: 52 | items: 53 | properties: 54 | namespace: 55 | properties: 56 | name: 57 | type: string 58 | required: 59 | - name 60 | type: object 61 | namespaceSelector: 62 | description: A label selector is a label query over a set of 63 | resources. The result of matchLabels and matchExpressions 64 | are ANDed. An empty label selector matches all objects. A 65 | null label selector matches no objects. 66 | properties: 67 | matchExpressions: 68 | description: matchExpressions is a list of label selector 69 | requirements. The requirements are ANDed. 70 | items: 71 | description: A label selector requirement is a selector 72 | that contains values, a key, and an operator that relates 73 | the key and values. 74 | properties: 75 | key: 76 | description: key is the label key that the selector 77 | applies to. 78 | type: string 79 | operator: 80 | description: operator represents a key's relationship 81 | to a set of values. Valid operators are In, NotIn, 82 | Exists and DoesNotExist. 83 | type: string 84 | values: 85 | description: values is an array of string values. 86 | If the operator is In or NotIn, the values array 87 | must be non-empty. If the operator is Exists or 88 | DoesNotExist, the values array must be empty. This 89 | array is replaced during a strategic merge patch. 90 | items: 91 | type: string 92 | type: array 93 | required: 94 | - key 95 | - operator 96 | type: object 97 | type: array 98 | matchLabels: 99 | additionalProperties: 100 | type: string 101 | description: matchLabels is a map of {key,value} pairs. 102 | A single {key,value} in the matchLabels map is equivalent 103 | to an element of matchExpressions, whose key field is 104 | "key", the operator is "In", and the values array contains 105 | only "value". The requirements are ANDed. 106 | type: object 107 | type: object 108 | x-kubernetes-map-type: atomic 109 | type: object 110 | type: array 111 | required: 112 | - source 113 | - targets 114 | type: object 115 | status: 116 | description: SyncSecretDefinitionStatus defines the observed state of 117 | SyncSecretDefinition 118 | type: object 119 | type: object 120 | served: true 121 | storage: true 122 | subresources: 123 | status: {} 124 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: access-manager 5 | labels: 6 | name: access-manager 7 | spec: 8 | selector: 9 | matchLabels: 10 | name: access-manager 11 | replicas: 1 12 | template: 13 | metadata: 14 | labels: 15 | name: access-manager 16 | spec: 17 | serviceAccountName: access-manager 18 | containers: 19 | - name: manager 20 | image: ckotzbauer/access-manager:latest 21 | imagePullPolicy: IfNotPresent 22 | args: 23 | - --enable-leader-election 24 | resources: 25 | requests: 26 | cpu: 10m 27 | memory: 128Mi 28 | limits: 29 | cpu: 50m 30 | memory: 128Mi 31 | securityContext: 32 | privileged: false 33 | runAsUser: 1001 34 | runAsNonRoot: false 35 | -------------------------------------------------------------------------------- /config/rbac/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: access-manager 5 | subjects: 6 | - kind: ServiceAccount 7 | name: access-manager 8 | namespace: default 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: cluster-admin 13 | -------------------------------------------------------------------------------- /config/rbac/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: access-manager 5 | -------------------------------------------------------------------------------- /config/samples/clusterrolebindings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: access-manager.io/v1beta1 2 | kind: RbacDefinition 3 | metadata: 4 | name: example-clusterrolebindings 5 | spec: 6 | cluster: 7 | - name: global-group-binding 8 | clusterRoleName: view 9 | subjects: 10 | - name: admin 11 | kind: Group 12 | -------------------------------------------------------------------------------- /config/samples/rolebindings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: access-manager.io/v1beta1 2 | kind: RbacDefinition 3 | metadata: 4 | name: example-rolebindings 5 | spec: 6 | namespaced: 7 | - namespace: 8 | name: my-product 9 | bindings: 10 | - roleName: my-product-management 11 | kind: Role 12 | subjects: 13 | - name: my-product-team 14 | kind: Group 15 | - name: devops-team 16 | kind: Group 17 | - namespaceSelector: 18 | matchLabels: 19 | ci: "true" 20 | bindings: 21 | - roleName: ci-deploy 22 | kind: ClusterRole 23 | subjects: 24 | - name: ci 25 | namespace: ci-service 26 | kind: ServiceAccount 27 | - namespaceSelector: 28 | matchExpressions: 29 | - key: customer 30 | operator: In 31 | values: 32 | - customer1 33 | - customer2 34 | bindings: 35 | - name: customer-rolebinding 36 | roleName: customer-role 37 | kind: ClusterRole 38 | subjects: 39 | - name: customer 40 | kind: Group 41 | -------------------------------------------------------------------------------- /config/samples/syncsecretdefinition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: access-manager.io/v1beta1 2 | kind: SyncSecretDefinition 3 | metadata: 4 | name: syncsecretdefinition-sample 5 | spec: 6 | source: 7 | name: source-secret 8 | namespace: default 9 | targets: 10 | - namespace: 11 | name: my-product 12 | - namespaceSelector: 13 | matchLabels: 14 | ci: "true" 15 | -------------------------------------------------------------------------------- /controllers/access-manager.io/namespace_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ckotzbauer/access-manager/pkg/reconciler" 7 | 8 | "github.com/go-logr/logr" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/rest" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 17 | ) 18 | 19 | // NamespaceReconciler is a wrapper for needed runtime-objects 20 | type NamespaceReconciler struct { 21 | Client client.Client 22 | Config *rest.Config 23 | Scheme *runtime.Scheme 24 | Logger logr.Logger 25 | } 26 | 27 | // Reconcile reads that state of the cluster for a Namespace object and makes changes based on the state 28 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 29 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 30 | func (r *NamespaceReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { 31 | _ = context.Background() 32 | _ = r.Logger.WithValues("namespace", request.Name) 33 | 34 | // Fetch the Namespace instance 35 | instance := &corev1.Namespace{} 36 | err := r.Client.Get(ctx, request.NamespacedName, instance) 37 | if err != nil { 38 | if errors.IsNotFound(err) { 39 | // Request object not found, could have been deleted after reconcile request. 40 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 41 | // Return and don't requeue 42 | return reconcile.Result{}, nil 43 | } 44 | // Error reading the object - requeue the request. 45 | r.Logger.Error(err, "Unexpected error occurred!") 46 | return reconcile.Result{}, err 47 | } 48 | 49 | r.Logger.Info("Reconciling Namespace", "Name", request.Name) 50 | rec := reconciler.Reconciler{Client: *kubernetes.NewForConfigOrDie(r.Config), ControllerClient: r.Client, Logger: r.Logger, Scheme: r.Scheme} 51 | return rec.ReconcileNamespace(instance) 52 | } 53 | 54 | func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error { 55 | return ctrl.NewControllerManagedBy(mgr). 56 | For(&corev1.Namespace{}). 57 | Complete(r) 58 | } 59 | -------------------------------------------------------------------------------- /controllers/access-manager.io/rbacdefinition_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | 22 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 23 | "github.com/ckotzbauer/access-manager/pkg/reconciler" 24 | 25 | "github.com/go-logr/logr" 26 | "k8s.io/apimachinery/pkg/api/errors" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/client-go/kubernetes" 29 | "k8s.io/client-go/rest" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | 34 | rbacdefinitionsv1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 35 | ) 36 | 37 | // RbacDefinitionReconciler reconciles a RbacDefinition object 38 | type RbacDefinitionReconciler struct { 39 | Client client.Client 40 | Config *rest.Config 41 | Scheme *runtime.Scheme 42 | Logger logr.Logger 43 | } 44 | 45 | func (r *RbacDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 46 | _ = context.Background() 47 | _ = r.Logger.WithValues("rbacdefinition", req.NamespacedName) 48 | 49 | // Fetch the RbacDefinition instance 50 | instance := &v1beta1.RbacDefinition{} 51 | err := r.Client.Get(ctx, req.NamespacedName, instance) 52 | if err != nil { 53 | if errors.IsNotFound(err) { 54 | // Request object not found, could have been deleted after reconcile request. 55 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 56 | // Return and don't requeue 57 | return reconcile.Result{}, nil 58 | } 59 | // Error reading the object - requeue the request. 60 | r.Logger.Error(err, "Unexpected error occurred!") 61 | return reconcile.Result{}, err 62 | } 63 | 64 | if instance.Spec.Paused { 65 | return reconcile.Result{}, nil 66 | } 67 | 68 | r.Logger.Info("Reconciling RbacDefinition", "Name", req.Name) 69 | rec := reconciler.Reconciler{Client: *kubernetes.NewForConfigOrDie(r.Config), Logger: r.Logger, Scheme: r.Scheme} 70 | return rec.ReconcileRbacDefinition(instance) 71 | } 72 | 73 | func (r *RbacDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error { 74 | return ctrl.NewControllerManagedBy(mgr). 75 | For(&rbacdefinitionsv1beta1.RbacDefinition{}). 76 | Complete(r) 77 | } 78 | -------------------------------------------------------------------------------- /controllers/access-manager.io/secret_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ckotzbauer/access-manager/pkg/reconciler" 7 | 8 | "github.com/go-logr/logr" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/rest" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 17 | ) 18 | 19 | // SecretReconciler is a wrapper for needed runtime-objects 20 | type SecretReconciler struct { 21 | Client client.Client 22 | Config *rest.Config 23 | Scheme *runtime.Scheme 24 | Logger logr.Logger 25 | } 26 | 27 | // Reconcile reads that state of the cluster for a Secret object and makes changes based on the state 28 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 29 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 30 | func (r *SecretReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { 31 | _ = context.Background() 32 | _ = r.Logger.WithValues("secret", request.Name) 33 | 34 | // Fetch the Secret instance 35 | instance := &corev1.Secret{} 36 | err := r.Client.Get(ctx, request.NamespacedName, instance) 37 | if err != nil { 38 | if errors.IsNotFound(err) { 39 | // Request object not found, could have been deleted after reconcile request. 40 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 41 | // Return and don't requeue 42 | return reconcile.Result{}, nil 43 | } 44 | // Error reading the object - requeue the request. 45 | r.Logger.Error(err, "Unexpected error occurred!") 46 | return reconcile.Result{}, err 47 | } 48 | 49 | rec := reconciler.Reconciler{Client: *kubernetes.NewForConfigOrDie(r.Config), ControllerClient: r.Client, Logger: r.Logger, Scheme: r.Scheme} 50 | return rec.ReconcileSecret(instance) 51 | } 52 | 53 | func (r *SecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 54 | return ctrl.NewControllerManagedBy(mgr). 55 | For(&corev1.Secret{}). 56 | Complete(r) 57 | } 58 | -------------------------------------------------------------------------------- /controllers/access-manager.io/serviceaccount_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ckotzbauer/access-manager/pkg/reconciler" 7 | 8 | "github.com/go-logr/logr" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/rest" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 17 | ) 18 | 19 | // ServiceAccountReconciler is a wrapper for needed runtime-objects 20 | type ServiceAccountReconciler struct { 21 | Client client.Client 22 | Config *rest.Config 23 | Scheme *runtime.Scheme 24 | Logger logr.Logger 25 | } 26 | 27 | // Reconcile reads that state of the cluster for a ServiceAccount object and makes changes based on the state 28 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 29 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 30 | func (r *ServiceAccountReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { 31 | _ = context.Background() 32 | _ = r.Logger.WithValues("serviceAccount", request.Name) 33 | 34 | // Fetch the ServiceAccount instance 35 | instance := &corev1.ServiceAccount{} 36 | err := r.Client.Get(ctx, request.NamespacedName, instance) 37 | if err != nil { 38 | if errors.IsNotFound(err) { 39 | // Request object not found, could have been deleted after reconcile request. 40 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 41 | // Return and don't requeue 42 | return reconcile.Result{}, nil 43 | } 44 | // Error reading the object - requeue the request. 45 | r.Logger.Error(err, "Unexpected error occurred!") 46 | return reconcile.Result{}, err 47 | } 48 | 49 | rec := reconciler.Reconciler{Client: *kubernetes.NewForConfigOrDie(r.Config), ControllerClient: r.Client, Logger: r.Logger, Scheme: r.Scheme} 50 | return rec.ReconcileServiceAccount(instance) 51 | } 52 | 53 | func (r *ServiceAccountReconciler) SetupWithManager(mgr ctrl.Manager) error { 54 | return ctrl.NewControllerManagedBy(mgr). 55 | For(&corev1.ServiceAccount{}). 56 | Complete(r) 57 | } 58 | -------------------------------------------------------------------------------- /controllers/access-manager.io/syncsecretdefinition_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-logr/logr" 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/client-go/kubernetes" 10 | "k8s.io/client-go/rest" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 14 | 15 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 16 | "github.com/ckotzbauer/access-manager/pkg/reconciler" 17 | ) 18 | 19 | // SyncSecretDefinitionReconciler reconciles a SyncSecretDefinition object 20 | type SyncSecretDefinitionReconciler struct { 21 | client.Client 22 | Config *rest.Config 23 | Logger logr.Logger 24 | Scheme *runtime.Scheme 25 | } 26 | 27 | func (r *SyncSecretDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 28 | _ = context.Background() 29 | _ = r.Logger.WithValues("syncsecretdefinition", req.NamespacedName) 30 | 31 | // Fetch the SecretSyncDefinition instance 32 | instance := &v1beta1.SyncSecretDefinition{} 33 | err := r.Client.Get(ctx, req.NamespacedName, instance) 34 | if err != nil { 35 | if errors.IsNotFound(err) { 36 | // Request object not found, could have been deleted after reconcile request. 37 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 38 | // Return and don't requeue 39 | return reconcile.Result{}, nil 40 | } 41 | // Error reading the object - requeue the request. 42 | r.Logger.Error(err, "Unexpected error occurred!") 43 | return reconcile.Result{}, err 44 | } 45 | 46 | if instance.Spec.Paused { 47 | return reconcile.Result{}, nil 48 | } 49 | 50 | r.Logger.Info("Reconciling SecretSyncDefinition", "Name", req.Name) 51 | rec := reconciler.Reconciler{Client: *kubernetes.NewForConfigOrDie(r.Config), Logger: r.Logger, Scheme: r.Scheme} 52 | return rec.ReconcileSyncSecretDefinition(instance) 53 | } 54 | 55 | func (r *SyncSecretDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error { 56 | return ctrl.NewControllerManagedBy(mgr). 57 | For(&v1beta1.SyncSecretDefinition{}). 58 | Complete(r) 59 | } 60 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Docs 2 | 3 | This Document documents the types introduced by the Access-Manager to be consumed by users. 4 | 5 | ## Table of Contents 6 | * [BindingsSpec](#bindingspec) 7 | * [ClusterSpec](#clusterspec) 8 | * [NamespacedSpec](#namespacedspec) 9 | * [NamespaceSpec](#namespacespec) 10 | * [RbacDefinition](#rbacdefinition) 11 | * [RbacDefinitionList](#rbacdefinitionlist) 12 | * [RbacDefinitionSpec](#rbacdefinitionspec) 13 | 14 | 15 | ## BindingsSpec 16 | 17 | BindingsSpec defines the name and "body" of a RoleBinding. 18 | 19 | | Field | Description | Scheme | Required | 20 | | ----- | ----------- | ------ | -------- | 21 | | name | Name of the RoleBinding. Optional, if not set `roleName` is used. | string | false | 22 | | roleName | Name of the Role or ClusterRole to reference. | string | true | 23 | | kind | Kind of the `roleName` Either `Role` or `ClusterRole`. | string | true | 24 | | allServiceAccounts | Whether all `ServiceAccount`s of this namespace should be included as subjects. | bool | false | 25 | | subjects | List of RBAC-Subjects. | [][rbacv1.Subject](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#subject-v1-rbac-authorization-k8s-io) | true | 26 | 27 | [Back to TOC](#table-of-contents) 28 | 29 | ## ClusterSpec 30 | 31 | ClusterSpec defines the name and "body" of a ClusterRoleBinding. 32 | 33 | | Field | Description | Scheme | Required | 34 | | ----- | ----------- | ------ | -------- | 35 | | name | Name of the ClusterRoleBinding. Optional, if not set `clusterRoleName` is used. | string | false | 36 | | clusterRoleName | Name of the ClusterRole to reference. | string | true | 37 | | subjects | List of RBAC-Subjects. | [][rbacv1.Subject](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#subject-v1-rbac-authorization-k8s-io) | true | 38 | 39 | [Back to TOC](#table-of-contents) 40 | 41 | ## NamespacedSpec 42 | 43 | NamespacedSpec describes a set of RoleBindings to create in different namespaces. 44 | 45 | | Field | Description | Scheme | Required | 46 | | ----- | ----------- | ------ | -------- | 47 | | namespace | Single namespace name. Optional, but one of `namespace` or `namespaceSelector` is required. | [NamespaceSpec](#namespacespec) | false | 48 | | namespaceSelector | LabelSelector. Optional, but one of `namespace` or `namespaceSelector` is required. | [metav1.LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-core) | false | 49 | | bindings | List of RoleBindings to create. | [][BindingsSpec](#bindingspec) | true | 50 | 51 | 52 | [Back to TOC](#table-of-contents) 53 | 54 | ## NamespaceSpec 55 | 56 | NamespaceSpec defines a name of a single namespace. 57 | 58 | | Field | Description | Scheme | Required | 59 | | ----- | ----------- | ------ | -------- | 60 | | name | Name of a single namespace. | string | true | 61 | 62 | 63 | [Back to TOC](#table-of-contents) 64 | 65 | ## RbacDefinition 66 | 67 | RbacDefinition is the definition object itself. 68 | 69 | | Field | Description | Scheme | Required | 70 | | ----- | ----------- | ------ | -------- | 71 | | metadata | | [metav1.ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta) | true | 72 | | spec | | [RbacDefinitionSpec](#rbacdefinitionspec) | true | 73 | 74 | [Back to TOC](#table-of-contents) 75 | 76 | ## RbacDefinitionList 77 | 78 | RbacDefinitionList is a list of RbacDefinitions. 79 | 80 | | Field | Description | Scheme | Required | 81 | | ----- | ----------- | ------ | -------- | 82 | | metadata | Standard list metadata. | [metav1.ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#listmeta-v1-meta) | true | 83 | | items | List of Definitions. | []*[RbacDefinition](#rbacdefinition) | true | 84 | 85 | [Back to TOC](#table-of-contents) 86 | 87 | ## RbacDefinitionSpec 88 | 89 | RbacDefinitionSpec defines namespace- and cluster-spec objects. 90 | 91 | | Field | Description | Scheme | Required | 92 | | ----- | ----------- | ------ | -------- | 93 | | paused | Represents whether any actions on the underlaying managed objects are being performed. Only delete actions will be performed. | bool | false | 94 | | namespaced | Optional, but one of `namespaced` or `cluster` is required. | [NamespacedSpec](#namespacedspec) | false | 95 | | cluster | Optional, but one of `namespaced` or `cluster` is required. | [ClusterSpec](#clusterspec) | false | 96 | 97 | 98 | [Back to TOC](#table-of-contents) 99 | -------------------------------------------------------------------------------- /e2e/integration_suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "k8s.io/client-go/dynamic" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/tools/clientcmd" 13 | logf "sigs.k8s.io/controller-runtime/pkg/log" 14 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 15 | ) 16 | 17 | func TestIntegration(t *testing.T) { 18 | RegisterFailHandler(Fail) 19 | RunSpecs(t, "Integration Suite") 20 | } 21 | 22 | var clientset *kubernetes.Clientset 23 | var client dynamic.Interface 24 | 25 | var _ = BeforeSuite(func() { 26 | done := make(chan interface{}) 27 | go func() { 28 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter))) 29 | 30 | kubeconfig := os.Getenv("KUBECONFIG") 31 | cwd, _ := os.Getwd() 32 | cfg, err := clientcmd.BuildConfigFromFlags("", filepath.Join(cwd, kubeconfig)) 33 | Expect(err).NotTo(HaveOccurred()) 34 | Expect(cfg).NotTo(BeNil()) 35 | 36 | client, err = dynamic.NewForConfig(cfg) 37 | Expect(err).NotTo(HaveOccurred()) 38 | Expect(client).NotTo(BeNil()) 39 | 40 | clientset, err = kubernetes.NewForConfig(cfg) 41 | Expect(err).NotTo(HaveOccurred()) 42 | Expect(clientset).NotTo(BeNil()) 43 | 44 | close(done) 45 | }() 46 | Eventually(done, 60).Should(BeClosed()) 47 | }) 48 | -------------------------------------------------------------------------------- /e2e/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 11 | 12 | b64 "encoding/base64" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | corev1 "k8s.io/api/core/v1" 17 | rbacv1 "k8s.io/api/rbac/v1" 18 | "k8s.io/apimachinery/pkg/api/errors" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/apimachinery/pkg/runtime/schema" 23 | "k8s.io/apimachinery/pkg/types" 24 | "k8s.io/apimachinery/pkg/util/strategicpatch" 25 | "k8s.io/client-go/dynamic" 26 | "k8s.io/client-go/kubernetes" 27 | ) 28 | 29 | var ( 30 | rbacDefGVR = schema.GroupVersionResource{ 31 | Group: "access-manager.io", 32 | Version: "v1beta1", 33 | Resource: "rbacdefinitions", 34 | } 35 | secretDefGVR = schema.GroupVersionResource{ 36 | Group: "access-manager.io", 37 | Version: "v1beta1", 38 | Resource: "syncsecretdefinitions", 39 | } 40 | ) 41 | 42 | type patchStringValue struct { 43 | Op string `json:"op"` 44 | Path string `json:"path"` 45 | Value string `json:"value"` 46 | } 47 | 48 | func getRoleBinding(c kubernetes.Clientset, ctx context.Context, name string, namespace string) (*rbacv1.RoleBinding, error) { 49 | return c.RbacV1().RoleBindings(namespace).Get(ctx, name, metav1.GetOptions{}) 50 | } 51 | 52 | func getClusterRoleBinding(c kubernetes.Clientset, ctx context.Context, name string) (*rbacv1.ClusterRoleBinding, error) { 53 | return c.RbacV1().ClusterRoleBindings().Get(ctx, name, metav1.GetOptions{}) 54 | } 55 | 56 | func getSecret(c kubernetes.Clientset, ctx context.Context, name string, namespace string) (*corev1.Secret, error) { 57 | return c.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) 58 | } 59 | 60 | func createServiceAccount(c kubernetes.Clientset, ctx context.Context, serviceAccount corev1.ServiceAccount) (*corev1.ServiceAccount, error) { 61 | return c.CoreV1().ServiceAccounts(serviceAccount.Namespace).Create(ctx, &serviceAccount, metav1.CreateOptions{}) 62 | } 63 | 64 | func deleteServiceAccount(c kubernetes.Clientset, ctx context.Context, namespace, name string) error { 65 | return c.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, metav1.DeleteOptions{}) 66 | } 67 | 68 | func patchNamespace(c kubernetes.Interface, ctx context.Context, cur, mod corev1.Namespace) error { 69 | curJson, err := json.Marshal(cur) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | modJson, err := json.Marshal(mod) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | patch, err := strategicpatch.CreateTwoWayMergePatch(curJson, modJson, corev1.Namespace{}) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if len(patch) == 0 || string(patch) == "{}" { 85 | return nil 86 | } 87 | 88 | _, err = c.CoreV1().Namespaces().Patch(ctx, cur.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) 89 | return err 90 | } 91 | 92 | func addNamespaceLabel(c kubernetes.Clientset, ctx context.Context, namespace string, labelKey string, labelValue string) error { 93 | current, _ := c.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) 94 | mod := current.DeepCopy() 95 | 96 | if mod.ObjectMeta.Labels == nil { 97 | mod.ObjectMeta.Labels = map[string]string{} 98 | } 99 | 100 | mod.ObjectMeta.Labels[labelKey] = labelValue 101 | return patchNamespace(&c, ctx, *current, *mod) 102 | } 103 | 104 | func deleteNamespaceLabel(c kubernetes.Clientset, ctx context.Context, namespace string, labelKey string) error { 105 | current, _ := c.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) 106 | mod := current.DeepCopy() 107 | 108 | delete(mod.ObjectMeta.Labels, labelKey) 109 | return patchNamespace(&c, ctx, *current, *mod) 110 | } 111 | 112 | func checkRoleBindingToBeEquivalent(rb rbacv1.RoleBinding, expected rbacv1.RoleBinding) { 113 | Expect(rb.Name).To(BeEquivalentTo(expected.Name)) 114 | Expect(rb.Namespace).To(BeEquivalentTo(expected.Namespace)) 115 | Expect(rb.RoleRef).To(BeEquivalentTo(expected.RoleRef)) 116 | Expect(rb.Subjects).To(BeEquivalentTo(expected.Subjects)) 117 | } 118 | 119 | func checkClusterRoleBindingToBeEquivalent(crb rbacv1.ClusterRoleBinding, expected rbacv1.ClusterRoleBinding) { 120 | Expect(crb.Name).To(BeEquivalentTo(expected.Name)) 121 | Expect(crb.RoleRef).To(BeEquivalentTo(expected.RoleRef)) 122 | Expect(crb.Subjects).To(BeEquivalentTo(expected.Subjects)) 123 | } 124 | 125 | func checkSecretToBeEquivalent(secret corev1.Secret, expected corev1.Secret) { 126 | Expect(secret.Name).To(BeEquivalentTo(expected.Name)) 127 | Expect(secret.Namespace).To(BeEquivalentTo(expected.Namespace)) 128 | Expect(secret.Type).To(BeEquivalentTo(secret.Type)) 129 | Expect(secret.Data).To(BeEquivalentTo(secret.Data)) 130 | Expect(secret.Immutable).To(BeEquivalentTo(expected.Immutable)) 131 | } 132 | 133 | func createRbacDefinition(c dynamic.Interface, ctx context.Context, def v1beta1.RbacDefinition) error { 134 | res := c.Resource(rbacDefGVR) 135 | unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&def) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | unstructuredObj["kind"] = "RbacDefinition" 141 | unstructuredObj["apiVersion"] = rbacDefGVR.Group + "/" + rbacDefGVR.Version 142 | log.Printf("Creating RbacDefinition %s", def.Name) 143 | _, err = res.Create(ctx, &unstructured.Unstructured{Object: unstructuredObj}, metav1.CreateOptions{}) 144 | if err != nil { 145 | fmt.Printf("Failed to create RbacDefinition %#v", def) 146 | } 147 | 148 | return err 149 | } 150 | 151 | func deleteRbacDefinition(c dynamic.Interface, ctx context.Context, def v1beta1.RbacDefinition) error { 152 | res := c.Resource(rbacDefGVR) 153 | 154 | log.Printf("Deleting RbacDefinition %s", def.Name) 155 | err := res.Delete(ctx, def.Name, metav1.DeleteOptions{}) 156 | if err != nil { 157 | fmt.Printf("Failed to delete RbacDefinition %#v", def) 158 | } 159 | 160 | return err 161 | } 162 | 163 | func createSyncSecretDefinition(c dynamic.Interface, ctx context.Context, def v1beta1.SyncSecretDefinition) error { 164 | res := c.Resource(secretDefGVR) 165 | unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&def) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | unstructuredObj["kind"] = "SyncSecretDefinition" 171 | unstructuredObj["apiVersion"] = secretDefGVR.Group + "/" + secretDefGVR.Version 172 | log.Printf("Creating SyncSecretDefinition %s", def.Name) 173 | _, err = res.Create(ctx, &unstructured.Unstructured{Object: unstructuredObj}, metav1.CreateOptions{}) 174 | if err != nil { 175 | fmt.Printf("Failed to create SyncSecretDefinition %#v", def) 176 | } 177 | 178 | return err 179 | } 180 | 181 | func deleteSyncSecretDefinition(c dynamic.Interface, ctx context.Context, def v1beta1.SyncSecretDefinition) error { 182 | res := c.Resource(secretDefGVR) 183 | 184 | log.Printf("Deleting SyncSecretDefinition %s", def.Name) 185 | err := res.Delete(ctx, def.Name, metav1.DeleteOptions{}) 186 | if err != nil { 187 | fmt.Printf("Failed to delete SyncSecretDefinition %#v", def) 188 | } 189 | 190 | return err 191 | } 192 | 193 | var _ = Describe("IntegrationTest", func() { 194 | var def1 v1beta1.RbacDefinition 195 | var def2 v1beta1.RbacDefinition 196 | var def3 v1beta1.RbacDefinition 197 | var secretDef1 v1beta1.SyncSecretDefinition 198 | var secretDef2 v1beta1.SyncSecretDefinition 199 | ctx := context.Background() 200 | 201 | def1 = v1beta1.RbacDefinition{ 202 | ObjectMeta: metav1.ObjectMeta{ 203 | Name: "rbac-def1", 204 | }, 205 | Spec: v1beta1.RbacDefinitionSpec{ 206 | Namespaced: []v1beta1.NamespacedSpec{ 207 | { 208 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"ci": "true"}}, 209 | Bindings: []v1beta1.BindingsSpec{ 210 | { 211 | Kind: "Role", 212 | RoleName: "test-role", 213 | Subjects: []rbacv1.Subject{ 214 | { 215 | Kind: "ServiceAccount", 216 | Name: "default", 217 | Namespace: "namespace1", 218 | }, 219 | }, 220 | }, 221 | }, 222 | }, 223 | }, 224 | }, 225 | } 226 | 227 | def2 = v1beta1.RbacDefinition{ 228 | ObjectMeta: metav1.ObjectMeta{ 229 | Name: "rbac-def2", 230 | }, 231 | Spec: v1beta1.RbacDefinitionSpec{ 232 | Cluster: []v1beta1.ClusterSpec{ 233 | { 234 | ClusterRoleName: "test-role", 235 | Subjects: []rbacv1.Subject{ 236 | { 237 | Kind: "ServiceAccount", 238 | Name: "default", 239 | Namespace: "namespace2", 240 | }, 241 | }, 242 | }, 243 | }, 244 | }, 245 | } 246 | 247 | def3 = v1beta1.RbacDefinition{ 248 | ObjectMeta: metav1.ObjectMeta{ 249 | Name: "rbac-def3", 250 | }, 251 | Spec: v1beta1.RbacDefinitionSpec{ 252 | Namespaced: []v1beta1.NamespacedSpec{ 253 | { 254 | Namespace: v1beta1.NamespaceSpec{ 255 | Name: "namespace4", 256 | }, 257 | Bindings: []v1beta1.BindingsSpec{ 258 | { 259 | Name: "test-rolebinding", 260 | RoleName: "test-role", 261 | Kind: "Role", 262 | AllServiceAccounts: true, 263 | Subjects: []rbacv1.Subject{}, 264 | }, 265 | }, 266 | }, 267 | }, 268 | }, 269 | } 270 | 271 | secretDef1 = v1beta1.SyncSecretDefinition{ 272 | ObjectMeta: metav1.ObjectMeta{ 273 | Name: "secret-def1", 274 | }, 275 | Spec: v1beta1.SyncSecretDefinitionSpec{ 276 | Source: v1beta1.SourceSpec{Namespace: "default", Name: "test-secret"}, 277 | Targets: []v1beta1.TargetSpec{ 278 | { 279 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"ci": "true"}}, 280 | }, 281 | }, 282 | }, 283 | } 284 | 285 | secretDef2 = v1beta1.SyncSecretDefinition{ 286 | ObjectMeta: metav1.ObjectMeta{ 287 | Name: "secret-def2", 288 | }, 289 | Spec: v1beta1.SyncSecretDefinitionSpec{ 290 | Source: v1beta1.SourceSpec{Namespace: "namespace2", Name: "test-secret2"}, 291 | Targets: []v1beta1.TargetSpec{ 292 | { 293 | Namespace: v1beta1.NamespaceSpec{Name: "namespace4"}, 294 | }, 295 | }, 296 | }, 297 | } 298 | 299 | Describe("RbacDefinition", func() { 300 | It("should apply new RoleBinding", func() { 301 | done := make(chan interface{}) 302 | go func() { 303 | defer GinkgoRecover() 304 | err := createRbacDefinition(client, ctx, def1) 305 | Expect(err).NotTo(HaveOccurred()) 306 | time.Sleep(3 * time.Second) 307 | 308 | expectedRb := rbacv1.RoleBinding{ 309 | ObjectMeta: metav1.ObjectMeta{Name: "test-role", Namespace: "namespace1"}, 310 | RoleRef: rbacv1.RoleRef{ 311 | APIGroup: "rbac.authorization.k8s.io", 312 | Name: "test-role", 313 | Kind: "Role", 314 | }, 315 | Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "default", Namespace: "namespace1"}}, 316 | } 317 | 318 | rb, err := getRoleBinding(*clientset, ctx, "test-role", "namespace1") 319 | Expect(err).NotTo(HaveOccurred()) 320 | checkRoleBindingToBeEquivalent(*rb, expectedRb) 321 | close(done) 322 | }() 323 | Eventually(done, 5).Should(BeClosed()) 324 | }) 325 | 326 | It("should apply new ClusterRoleBinding", func() { 327 | done := make(chan interface{}) 328 | go func() { 329 | defer GinkgoRecover() 330 | err := createRbacDefinition(client, ctx, def2) 331 | Expect(err).NotTo(HaveOccurred()) 332 | time.Sleep(3 * time.Second) 333 | 334 | expectedCrb := rbacv1.ClusterRoleBinding{ 335 | ObjectMeta: metav1.ObjectMeta{Name: "test-role"}, 336 | RoleRef: rbacv1.RoleRef{ 337 | APIGroup: "rbac.authorization.k8s.io", 338 | Name: "test-role", 339 | Kind: "ClusterRole", 340 | }, 341 | Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "default", Namespace: "namespace2"}}, 342 | } 343 | 344 | crb, err := getClusterRoleBinding(*clientset, ctx, "test-role") 345 | Expect(err).NotTo(HaveOccurred()) 346 | checkClusterRoleBindingToBeEquivalent(*crb, expectedCrb) 347 | close(done) 348 | }() 349 | Eventually(done, 5).Should(BeClosed()) 350 | }) 351 | 352 | It("should delete ClusterRoleBinding on definition removal", func() { 353 | done := make(chan interface{}) 354 | go func() { 355 | defer GinkgoRecover() 356 | err := deleteRbacDefinition(client, ctx, def2) 357 | Expect(err).NotTo(HaveOccurred()) 358 | time.Sleep(3 * time.Second) 359 | 360 | _, err = getClusterRoleBinding(*clientset, ctx, "test-role") 361 | Expect(errors.IsNotFound(err)).To(BeTrue()) 362 | close(done) 363 | }() 364 | Eventually(done, 5).Should(BeClosed()) 365 | }) 366 | 367 | It("should create a RoleBinding if namespace is labeled", func() { 368 | done := make(chan interface{}) 369 | go func() { 370 | defer GinkgoRecover() 371 | err := addNamespaceLabel(*clientset, ctx, "namespace3", "ci", "true") 372 | Expect(err).NotTo(HaveOccurred()) 373 | time.Sleep(3 * time.Second) 374 | 375 | expectedRb := rbacv1.RoleBinding{ 376 | ObjectMeta: metav1.ObjectMeta{Name: "test-role", Namespace: "namespace3"}, 377 | RoleRef: rbacv1.RoleRef{ 378 | APIGroup: "rbac.authorization.k8s.io", 379 | Name: "test-role", 380 | Kind: "Role", 381 | }, 382 | Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "default", Namespace: "namespace1"}}, 383 | } 384 | 385 | rb, err := getRoleBinding(*clientset, ctx, "test-role", "namespace3") 386 | Expect(err).NotTo(HaveOccurred()) 387 | checkRoleBindingToBeEquivalent(*rb, expectedRb) 388 | close(done) 389 | }() 390 | Eventually(done, 5).Should(BeClosed()) 391 | }) 392 | 393 | It("should delete a RoleBinding if namespace is unlabeled", func() { 394 | done := make(chan interface{}) 395 | go func() { 396 | defer GinkgoRecover() 397 | err := deleteNamespaceLabel(*clientset, ctx, "namespace3", "ci") 398 | Expect(err).NotTo(HaveOccurred()) 399 | time.Sleep(3 * time.Second) 400 | 401 | _, err = getRoleBinding(*clientset, ctx, "test-role", "namespace3") 402 | Expect(errors.IsNotFound(err)).To(BeTrue()) 403 | close(done) 404 | }() 405 | Eventually(done, 5).Should(BeClosed()) 406 | }) 407 | 408 | It("should modify RoleBinding on ServiceAccount creation", func() { 409 | done := make(chan interface{}) 410 | go func() { 411 | defer GinkgoRecover() 412 | err := createRbacDefinition(client, ctx, def3) 413 | Expect(err).NotTo(HaveOccurred()) 414 | time.Sleep(3 * time.Second) 415 | 416 | expectedRb := rbacv1.RoleBinding{ 417 | ObjectMeta: metav1.ObjectMeta{Name: "test-rolebinding", Namespace: "namespace4"}, 418 | RoleRef: rbacv1.RoleRef{ 419 | APIGroup: "rbac.authorization.k8s.io", 420 | Name: "test-role", 421 | Kind: "Role", 422 | }, 423 | Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "default", Namespace: ""}}, 424 | } 425 | 426 | rb, err := getRoleBinding(*clientset, ctx, "test-rolebinding", "namespace4") 427 | Expect(err).NotTo(HaveOccurred()) 428 | checkRoleBindingToBeEquivalent(*rb, expectedRb) 429 | 430 | createServiceAccount(*clientset, ctx, corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "new-sa", Namespace: "namespace4"}}) 431 | time.Sleep(3 * time.Second) 432 | 433 | expectedRb = rbacv1.RoleBinding{ 434 | ObjectMeta: metav1.ObjectMeta{Name: "test-rolebinding", Namespace: "namespace4"}, 435 | RoleRef: rbacv1.RoleRef{ 436 | APIGroup: "rbac.authorization.k8s.io", 437 | Name: "test-role", 438 | Kind: "Role", 439 | }, 440 | Subjects: []rbacv1.Subject{ 441 | {Kind: "ServiceAccount", Name: "default", Namespace: ""}, 442 | {Kind: "ServiceAccount", Name: "new-sa", Namespace: ""}, 443 | }, 444 | } 445 | 446 | rb, err = getRoleBinding(*clientset, ctx, "test-rolebinding", "namespace4") 447 | Expect(err).NotTo(HaveOccurred()) 448 | checkRoleBindingToBeEquivalent(*rb, expectedRb) 449 | close(done) 450 | }() 451 | Eventually(done, 10).Should(BeClosed()) 452 | }) 453 | }) 454 | 455 | Describe("SyncSecretDefinition", func() { 456 | It("should apply new Secret", func() { 457 | done := make(chan interface{}) 458 | go func() { 459 | defer GinkgoRecover() 460 | err := createSyncSecretDefinition(client, ctx, secretDef1) 461 | Expect(err).NotTo(HaveOccurred()) 462 | err = createSyncSecretDefinition(client, ctx, secretDef2) 463 | Expect(err).NotTo(HaveOccurred()) 464 | time.Sleep(3 * time.Second) 465 | 466 | expectedSecret := corev1.Secret{ 467 | ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "namespace1"}, 468 | Type: corev1.SecretTypeOpaque, 469 | Data: map[string][]byte{"key2": []byte(b64.StdEncoding.EncodeToString([]byte("value2")))}, 470 | } 471 | 472 | secret, err := getSecret(*clientset, ctx, "test-secret", "namespace1") 473 | Expect(err).NotTo(HaveOccurred()) 474 | checkSecretToBeEquivalent(*secret, expectedSecret) 475 | close(done) 476 | }() 477 | Eventually(done, 5).Should(BeClosed()) 478 | }) 479 | 480 | It("should delete Secrets on definition removal", func() { 481 | done := make(chan interface{}) 482 | go func() { 483 | defer GinkgoRecover() 484 | _, err := getSecret(*clientset, ctx, "test-secret2", "namespace4") 485 | Expect(err).NotTo(HaveOccurred()) 486 | 487 | err = deleteSyncSecretDefinition(client, ctx, secretDef2) 488 | Expect(err).NotTo(HaveOccurred()) 489 | time.Sleep(3 * time.Second) 490 | 491 | _, err = getSecret(*clientset, ctx, "test-secret2", "namespace4") 492 | Expect(errors.IsNotFound(err)).To(BeTrue()) 493 | close(done) 494 | }() 495 | Eventually(done, 5).Should(BeClosed()) 496 | }) 497 | 498 | It("should create a Secret if namespace is labeled", func() { 499 | done := make(chan interface{}) 500 | go func() { 501 | defer GinkgoRecover() 502 | err := addNamespaceLabel(*clientset, ctx, "namespace3", "ci", "true") 503 | Expect(err).NotTo(HaveOccurred()) 504 | time.Sleep(3 * time.Second) 505 | 506 | expectedSecret := corev1.Secret{ 507 | ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "namespace3"}, 508 | Type: corev1.SecretTypeOpaque, 509 | Data: map[string][]byte{"key2": []byte(b64.StdEncoding.EncodeToString([]byte("value2")))}, 510 | } 511 | 512 | secret, err := getSecret(*clientset, ctx, "test-secret", "namespace3") 513 | Expect(err).NotTo(HaveOccurred()) 514 | checkSecretToBeEquivalent(*secret, expectedSecret) 515 | close(done) 516 | }() 517 | Eventually(done, 5).Should(BeClosed()) 518 | }) 519 | 520 | It("should not touch secrets unchanged", func() { 521 | done := make(chan interface{}) 522 | go func() { 523 | defer GinkgoRecover() 524 | existingSecret, err := getSecret(*clientset, ctx, "test-secret", "namespace3") 525 | Expect(err).NotTo(HaveOccurred()) 526 | 527 | err = addNamespaceLabel(*clientset, ctx, "namespace3", "unspecified", "label") 528 | Expect(err).NotTo(HaveOccurred()) 529 | time.Sleep(3 * time.Second) 530 | 531 | secret, err := getSecret(*clientset, ctx, "test-secret", "namespace3") 532 | Expect(err).NotTo(HaveOccurred()) 533 | Expect(existingSecret.GetUID()).To(BeEquivalentTo(secret.GetUID())) 534 | close(done) 535 | }() 536 | Eventually(done, 5).Should(BeClosed()) 537 | }) 538 | 539 | It("should delete a Secret if namespace is unlabeled", func() { 540 | done := make(chan interface{}) 541 | go func() { 542 | defer GinkgoRecover() 543 | err := deleteNamespaceLabel(*clientset, ctx, "namespace3", "ci") 544 | Expect(err).NotTo(HaveOccurred()) 545 | time.Sleep(3 * time.Second) 546 | 547 | _, err = getSecret(*clientset, ctx, "test-secret", "namespace3") 548 | Expect(errors.IsNotFound(err)).To(BeTrue()) 549 | close(done) 550 | }() 551 | Eventually(done, 5).Should(BeClosed()) 552 | }) 553 | }) 554 | }) 555 | -------------------------------------------------------------------------------- /e2e/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | KIND=$1 5 | K8S_VERSION=$2 6 | 7 | cd .. 8 | 9 | ${KIND} create cluster --image kindest/node:v${K8S_VERSION} 10 | ${KIND} get kubeconfig >e2e/kind-kubeconfig 11 | export KUBECONFIG=e2e/kind-kubeconfig 12 | 13 | ${KIND} load docker-image ckotzbauer/access-manager:latest 14 | 15 | make install deploy 16 | 17 | sleep 20 18 | kubectl wait --for=condition=Ready pod -l name=access-manager --timeout=60s 19 | OPERATOR_POD=$(kubectl get pod -l name=access-manager -o jsonpath='{.items[*].metadata.name}') 20 | 21 | kubectl create ns namespace1 22 | kubectl create ns namespace2 23 | kubectl create ns namespace3 24 | kubectl create ns namespace4 25 | 26 | kubectl label ns namespace1 ci=true 27 | kubectl create secret generic test-secret --from-literal key2=value2 28 | kubectl create secret generic test-secret2 -n namespace2 --from-literal key7=value14 29 | 30 | cd e2e 31 | export KUBECONFIG=kind-kubeconfig 32 | go test 33 | 34 | kubectl logs $OPERATOR_POD 35 | 36 | ${KIND} delete cluster 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ckotzbauer/access-manager 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.2 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.34.2 9 | go.uber.org/zap v1.27.0 10 | k8s.io/api v0.31.0 11 | k8s.io/apimachinery v0.31.0 12 | k8s.io/client-go v0.31.0 13 | sigs.k8s.io/controller-runtime v0.19.0 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.11.0 // 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.19.6 // indirect 26 | github.com/go-openapi/jsonreference v0.20.2 // indirect 27 | github.com/go-openapi/swag v0.22.4 // indirect 28 | github.com/gogo/protobuf v1.3.2 // indirect 29 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 30 | github.com/golang/protobuf v1.5.4 // indirect 31 | github.com/google/gnostic-models v0.6.8 // indirect 32 | github.com/google/go-cmp v0.6.0 // indirect 33 | github.com/google/gofuzz v1.2.0 // indirect 34 | github.com/google/uuid v1.6.0 // indirect 35 | github.com/imdario/mergo v0.3.12 // indirect 36 | github.com/josharian/intern v1.0.0 // indirect 37 | github.com/json-iterator/go v1.1.12 // indirect 38 | github.com/mailru/easyjson v0.7.7 // indirect 39 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 41 | github.com/modern-go/reflect2 v1.0.2 // indirect 42 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 43 | github.com/nxadm/tail v1.4.8 // indirect 44 | github.com/pkg/errors v0.9.1 // indirect 45 | github.com/prometheus/client_golang v1.19.1 // indirect 46 | github.com/prometheus/client_model v0.6.1 // indirect 47 | github.com/prometheus/common v0.55.0 // indirect 48 | github.com/prometheus/procfs v0.15.1 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | github.com/x448/float16 v0.8.4 // indirect 51 | go.uber.org/multierr v1.11.0 // indirect 52 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 53 | golang.org/x/net v0.28.0 // indirect 54 | golang.org/x/oauth2 v0.21.0 // indirect 55 | golang.org/x/sys v0.25.0 // indirect 56 | golang.org/x/term v0.24.0 // indirect 57 | golang.org/x/text v0.18.0 // indirect 58 | golang.org/x/time v0.3.0 // indirect 59 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 60 | google.golang.org/appengine v1.6.7 // indirect 61 | google.golang.org/protobuf v1.34.2 // indirect 62 | gopkg.in/inf.v0 v0.9.1 // indirect 63 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 64 | gopkg.in/yaml.v2 v2.4.0 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | k8s.io/apiextensions-apiserver v0.31.0 // indirect 67 | k8s.io/klog/v2 v2.130.1 // indirect 68 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 69 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 70 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 71 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 72 | sigs.k8s.io/yaml v1.4.0 // indirect 73 | ) 74 | 75 | replace ( 76 | github.com/emicklei/go-restful/v3 => github.com/emicklei/go-restful/v3 v3.12.1 77 | golang.org/x/net => golang.org/x/net v0.29.0 78 | ) 79 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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 v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 16 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 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.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 21 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 22 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 23 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 24 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 25 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 26 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 27 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 28 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 29 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 30 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 31 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 32 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 33 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 34 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 35 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 36 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 37 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 38 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 39 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 40 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 41 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 42 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 45 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 48 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 49 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 50 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 51 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 52 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 53 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 54 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 55 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 56 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 57 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 58 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 59 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 60 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 61 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 62 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 63 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 64 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 65 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 66 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= 67 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 68 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 69 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 70 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 71 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 72 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 73 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 74 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 75 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 76 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 79 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 80 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 81 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 82 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 83 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 84 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 85 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 86 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 87 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 88 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 89 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 90 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 91 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 92 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 94 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 95 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 96 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 97 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 98 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 99 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 100 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 101 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 102 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 103 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 104 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 105 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 106 | github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= 107 | github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= 108 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 109 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 110 | github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= 111 | github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= 112 | github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os= 113 | github.com/onsi/gomega v1.34.0/go.mod h1:MIKI8c+f+QLWk+hxbePD4i0LMJSExPaZOVfkoex4cAo= 114 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 115 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 116 | github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= 117 | github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= 118 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 119 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 120 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 121 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 122 | github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= 123 | github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= 124 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 125 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 126 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 127 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 128 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 129 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 130 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 131 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 132 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 133 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 134 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 135 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 136 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 137 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 138 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 139 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 140 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 141 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 144 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 145 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 146 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 147 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 148 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 149 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 150 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 151 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 152 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 153 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 154 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 155 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 156 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 157 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 158 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 159 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 160 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 161 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 162 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 163 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 164 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 165 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 166 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 167 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 168 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= 169 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= 170 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 171 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 172 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 173 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 174 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 175 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 176 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 177 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 178 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 179 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 180 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 181 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 182 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 183 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 184 | golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= 185 | golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= 186 | golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= 187 | golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 188 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 189 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 190 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 191 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 192 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 194 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 195 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 196 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 197 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 198 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 200 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 205 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 206 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 207 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 208 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 209 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 210 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 211 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 212 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 213 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 214 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 215 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 216 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 217 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 218 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 219 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 220 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 221 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 222 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 223 | golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= 224 | golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= 225 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 226 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 227 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 228 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 229 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 230 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 231 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 232 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 233 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 234 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 235 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 236 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 237 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 238 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 239 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 240 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 241 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 242 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 243 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 244 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 245 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 246 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 248 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 250 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 251 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 252 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 253 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 254 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 255 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 256 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 257 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 258 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 259 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 260 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 261 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 262 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 263 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 264 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 265 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 266 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 267 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 268 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 269 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 270 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 271 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 272 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 273 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 274 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 275 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 276 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 277 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 278 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 279 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 280 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 281 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 282 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 283 | k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= 284 | k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= 285 | k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= 286 | k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= 287 | k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= 288 | k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= 289 | k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= 290 | k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= 291 | k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= 292 | k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= 293 | k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= 294 | k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 295 | k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= 296 | k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 297 | k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= 298 | k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= 299 | k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= 300 | k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= 301 | k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= 302 | k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= 303 | k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= 304 | k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= 305 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 306 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 307 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 308 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 309 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 310 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 311 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 312 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 313 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= 314 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 315 | sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= 316 | sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= 317 | sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= 318 | sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= 319 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 320 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 321 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 322 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 323 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 324 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 325 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckotzbauer/access-manager/0e8760ef2561cbaf7b0715562560ee422a75f546/hack/boilerplate.go.txt -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "fmt" 22 | "os" 23 | rtm "runtime" 24 | 25 | "go.uber.org/zap/zapcore" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 29 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 33 | 34 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 35 | controllers "github.com/ckotzbauer/access-manager/controllers/access-manager.io" 36 | // +kubebuilder:scaffold:imports 37 | ) 38 | 39 | var ( 40 | scheme = runtime.NewScheme() 41 | setupLog = ctrl.Log.WithName("setup") 42 | ) 43 | 44 | var ( 45 | // Version sets the current Operator version 46 | Version = "0.0.1" 47 | Commit = "main" 48 | Date = "" 49 | BuiltBy = "" 50 | ) 51 | 52 | func init() { 53 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 54 | 55 | utilruntime.Must(v1beta1.AddToScheme(scheme)) 56 | // +kubebuilder:scaffold:scheme 57 | } 58 | 59 | func printVersion() { 60 | setupLog.Info(fmt.Sprintf("Version: %s", Version)) 61 | setupLog.Info(fmt.Sprintf("Commit: %s", Commit)) 62 | setupLog.Info(fmt.Sprintf("Buit at: %s", Date)) 63 | setupLog.Info(fmt.Sprintf("Buit by: %s", BuiltBy)) 64 | setupLog.Info(fmt.Sprintf("Go Version: %s", rtm.Version())) 65 | } 66 | 67 | func main() { 68 | var enableLeaderElection bool 69 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 70 | "Enable leader election for controller manager. "+ 71 | "Enabling this will ensure there is only one active controller manager.") 72 | flag.Parse() 73 | 74 | ctrl.SetLogger(zap.New(zap.UseDevMode(true), func(o *zap.Options) { o.TimeEncoder = zapcore.ISO8601TimeEncoder })) 75 | 76 | printVersion() 77 | 78 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 79 | Scheme: scheme, 80 | Metrics: server.Options{ 81 | BindAddress: "0", 82 | }, 83 | LeaderElection: enableLeaderElection, 84 | LeaderElectionID: "85a69c09.access-manager.io", 85 | }) 86 | if err != nil { 87 | setupLog.Error(err, "unable to start access-manager") 88 | os.Exit(1) 89 | } 90 | 91 | if err = (&controllers.RbacDefinitionReconciler{ 92 | Client: mgr.GetClient(), 93 | Logger: ctrl.Log.WithName("controllers").WithName("RbacDefinition"), 94 | Scheme: mgr.GetScheme(), 95 | Config: mgr.GetConfig(), 96 | }).SetupWithManager(mgr); err != nil { 97 | setupLog.Error(err, "unable to create controller", "controller", "RbacDefinition") 98 | os.Exit(1) 99 | } 100 | 101 | if err = (&controllers.NamespaceReconciler{ 102 | Client: mgr.GetClient(), 103 | Logger: ctrl.Log.WithName("controllers").WithName("Namespace"), 104 | Scheme: mgr.GetScheme(), 105 | Config: mgr.GetConfig(), 106 | }).SetupWithManager(mgr); err != nil { 107 | setupLog.Error(err, "unable to create controller", "controller", "Namespace") 108 | os.Exit(1) 109 | } 110 | 111 | if err = (&controllers.ServiceAccountReconciler{ 112 | Client: mgr.GetClient(), 113 | Logger: ctrl.Log.WithName("controllers").WithName("ServiceAccount"), 114 | Scheme: mgr.GetScheme(), 115 | Config: mgr.GetConfig(), 116 | }).SetupWithManager(mgr); err != nil { 117 | setupLog.Error(err, "unable to create controller", "controller", "ServiceAccount") 118 | os.Exit(1) 119 | } 120 | 121 | if err = (&controllers.SyncSecretDefinitionReconciler{ 122 | Client: mgr.GetClient(), 123 | Logger: ctrl.Log.WithName("controllers").WithName("SyncSecretDefinition"), 124 | Scheme: mgr.GetScheme(), 125 | Config: mgr.GetConfig(), 126 | }).SetupWithManager(mgr); err != nil { 127 | setupLog.Error(err, "unable to create controller", "controller", "SyncSecretDefinition") 128 | os.Exit(1) 129 | } 130 | 131 | if err = (&controllers.SecretReconciler{ 132 | Client: mgr.GetClient(), 133 | Logger: ctrl.Log.WithName("controllers").WithName("Secret"), 134 | Scheme: mgr.GetScheme(), 135 | Config: mgr.GetConfig(), 136 | }).SetupWithManager(mgr); err != nil { 137 | setupLog.Error(err, "unable to create controller", "controller", "Secret") 138 | os.Exit(1) 139 | } 140 | // +kubebuilder:scaffold:builder 141 | 142 | setupLog.Info("starting access-manager") 143 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 144 | setupLog.Error(err, "problem running access-manager") 145 | os.Exit(1) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /pkg/reconciler/common.go: -------------------------------------------------------------------------------- 1 | package reconciler 2 | 3 | import ( 4 | "context" 5 | 6 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 7 | 8 | "github.com/go-logr/logr" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/client-go/kubernetes" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 15 | ) 16 | 17 | // Reconciler runtime-object 18 | type Reconciler struct { 19 | Client kubernetes.Clientset 20 | ControllerClient client.Client 21 | Scheme *runtime.Scheme 22 | Logger logr.Logger 23 | } 24 | 25 | // ReconcileNamespace applies all desired changes of the Namespace 26 | func (r *Reconciler) ReconcileNamespace(instance *corev1.Namespace) (reconcile.Result, error) { 27 | result1, err1 := r.processRbacDefinitions() 28 | result2, err2 := r.processSecretDefinitions() 29 | 30 | if err1 != nil { 31 | return result1, err1 32 | } 33 | 34 | if err2 != nil { 35 | return result2, err2 36 | } 37 | 38 | return reconcile.Result{}, nil 39 | } 40 | 41 | func (r *Reconciler) processRbacDefinitions() (reconcile.Result, error) { 42 | list := &v1beta1.RbacDefinitionList{} 43 | err := r.ControllerClient.List(context.Background(), list) 44 | 45 | if err != nil { 46 | r.Logger.Error(err, "Unexpected error occurred!") 47 | return reconcile.Result{}, err 48 | } 49 | 50 | for _, def := range list.Items { 51 | if def.Spec.Paused { 52 | continue 53 | } 54 | 55 | _, err = r.ReconcileRbacDefinition(&def) 56 | 57 | if err != nil { 58 | return reconcile.Result{}, err 59 | } 60 | } 61 | 62 | return reconcile.Result{}, nil 63 | } 64 | 65 | func (r *Reconciler) processSecretDefinitions() (reconcile.Result, error) { 66 | list := &v1beta1.SyncSecretDefinitionList{} 67 | err := r.ControllerClient.List(context.Background(), list) 68 | 69 | if err != nil { 70 | r.Logger.Error(err, "Unexpected error occurred!") 71 | return reconcile.Result{}, err 72 | } 73 | 74 | for _, def := range list.Items { 75 | if def.Spec.Paused { 76 | continue 77 | } 78 | 79 | _, err = r.ReconcileSyncSecretDefinition(&def) 80 | 81 | if err != nil { 82 | return reconcile.Result{}, err 83 | } 84 | } 85 | 86 | return reconcile.Result{}, nil 87 | } 88 | 89 | // HasNamedOwner returns true if the owner array includes a object of the givien kind and name 90 | func HasNamedOwner(refs []metav1.OwnerReference, kind, name string) bool { 91 | for _, ref := range refs { 92 | if ref.Controller != nil && *ref.Controller && ref.Kind == kind && (name == "" || name == ref.Name) { 93 | return true 94 | } 95 | } 96 | 97 | return false 98 | } 99 | 100 | // GetRelevantNamespaces returns a filtered list of namespaces matching the NamespacedSpec 101 | func (r *Reconciler) GetRelevantNamespaces(selector metav1.LabelSelector, nameSpec v1beta1.NamespaceSpec) []corev1.Namespace { 102 | if selector.MatchLabels != nil || len(selector.MatchExpressions) > 0 { 103 | selector, err := metav1.LabelSelectorAsSelector(&selector) 104 | if err != nil { 105 | r.Logger.WithValues("Selector", selector).Error(err, "Could not parse LabelSelector or MatchExpression.") 106 | return nil 107 | } 108 | 109 | listOptions := metav1.ListOptions{LabelSelector: selector.String()} 110 | namespaces, err := r.Client.CoreV1().Namespaces().List(context.Background(), listOptions) 111 | if err != nil { 112 | r.Logger.Error(err, "Could not list namespaces.") 113 | return nil 114 | } 115 | 116 | return namespaces.Items 117 | 118 | } else if nameSpec.Name != "" { 119 | namespace, err := r.Client.CoreV1().Namespaces().Get(context.Background(), nameSpec.Name, metav1.GetOptions{}) 120 | if err != nil { 121 | r.Logger.WithValues("NsName", nameSpec.Name).Error(err, "Could not find Namespace with name.") 122 | return nil 123 | } 124 | 125 | return []corev1.Namespace{*namespace} 126 | } else { 127 | r.Logger.Error(nil, "Invalid role binding, namespace or namespaceSelector required") 128 | return nil 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /pkg/reconciler/reconciler.go: -------------------------------------------------------------------------------- 1 | package reconciler 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 9 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 10 | 11 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 12 | "github.com/ckotzbauer/access-manager/pkg/util" 13 | 14 | corev1 "k8s.io/api/core/v1" 15 | rbacv1 "k8s.io/api/rbac/v1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | ) 18 | 19 | var rbacName = "RbacDefinition" 20 | 21 | // ReconcileServiceAccount applies all desired changes of the ServiceAccount 22 | func (r *Reconciler) ReconcileServiceAccount(instance *corev1.ServiceAccount) (reconcile.Result, error) { 23 | list := &v1beta1.RbacDefinitionList{} 24 | err := r.ControllerClient.List(context.Background(), list) 25 | 26 | if err != nil { 27 | r.Logger.Error(err, "Unexpected error occurred!") 28 | return reconcile.Result{}, err 29 | } 30 | 31 | for _, def := range list.Items { 32 | if def.Spec.Paused { 33 | continue 34 | } 35 | 36 | if !r.IsServiceAccountRelevant(def, instance.Namespace) { 37 | continue 38 | } 39 | 40 | _, err = r.ReconcileRbacDefinition(&def) 41 | 42 | if err != nil { 43 | return reconcile.Result{}, err 44 | } 45 | } 46 | 47 | return reconcile.Result{}, nil 48 | } 49 | 50 | // ReconcileRbacDefinition applies all desired changes of the RbacDefinition 51 | func (r *Reconciler) ReconcileRbacDefinition(instance *v1beta1.RbacDefinition) (reconcile.Result, error) { 52 | // Define all (Cluster)RoleBindings objects 53 | roleBindingsToCreate := r.BuildAllRoleBindings(instance) 54 | clusterRoleBindingsToCreate := r.BuildAllClusterRoleBindings(instance) 55 | 56 | r.RemoveAllDeletableRoleBindings(instance.Name, roleBindingsToCreate) 57 | r.RemoveAllDeletableClusterRoleBindings(instance.Name, clusterRoleBindingsToCreate) 58 | 59 | for _, rb := range roleBindingsToCreate { 60 | // Set RbacDefinition instance as the owner and controller 61 | if err := controllerutil.SetControllerReference(instance, &rb, r.Scheme); err != nil { 62 | r.Logger.WithValues("RoleBinding", rb.Namespace+"/"+rb.Name).Error(err, "Failed to set controllerReference.") 63 | continue 64 | } 65 | 66 | if _, err := r.CreateOrRecreateRoleBinding(rb); err != nil { 67 | r.Logger.WithValues("RoleBinding", rb.Namespace+"/"+rb.Name).Error(err, "Failed to reconcile RoleBinding.") 68 | } 69 | } 70 | 71 | for _, crb := range clusterRoleBindingsToCreate { 72 | // Set RbacDefinition instance as the owner and controller 73 | if err := controllerutil.SetControllerReference(instance, &crb, r.Scheme); err != nil { 74 | r.Logger.WithValues("ClusterRoleBinding", crb.Name).Error(err, "Failed to set controllerReference.") 75 | continue 76 | } 77 | 78 | if _, err := r.CreateOrRecreateClusterRoleBinding(crb); err != nil { 79 | r.Logger.WithValues("ClusterRoleBinding", crb.Name).Error(err, "Failed to reconcile ClusterRoleBinding.") 80 | } 81 | } 82 | 83 | return reconcile.Result{}, nil 84 | } 85 | 86 | // RemoveAllDeletableRoleBindings deletes all RoleBindings which wouldn't be created again. 87 | func (r *Reconciler) RemoveAllDeletableRoleBindings(defName string, roleBindingsToCreate []rbacv1.RoleBinding) { 88 | roleBindingsToDelete, err := r.getRoleBindingsToDelete(defName, roleBindingsToCreate) 89 | 90 | if err != nil { 91 | r.Logger.Error(err, "Failed to fetch all deletable RoleBindings.") 92 | } 93 | 94 | for _, rb := range roleBindingsToDelete { 95 | r.Logger.Info("Deleting RoleBinding", "Name", fmt.Sprintf("%s/%s", rb.Namespace, rb.Name)) 96 | err = r.Client.RbacV1().RoleBindings(rb.Namespace).Delete(context.Background(), rb.Name, metav1.DeleteOptions{}) 97 | 98 | if err != nil { 99 | r.Logger.WithValues("Name", fmt.Sprintf("%s/%s", rb.Namespace, rb.Name)).Error(err, "Failed to delete RoleBinding.") 100 | } 101 | } 102 | } 103 | 104 | // RemoveAllDeletableClusterRoleBindings deletes all ClusterRoleBindings which wouldn't be created again. 105 | func (r *Reconciler) RemoveAllDeletableClusterRoleBindings(defName string, clusterRoleBindingsToCreate []rbacv1.ClusterRoleBinding) { 106 | clusterRoleBindingsToDelete, err := r.getClusterRoleBindingsToDelete(defName, clusterRoleBindingsToCreate) 107 | 108 | if err != nil { 109 | r.Logger.Error(err, "Failed to fetch all deletable ClusterRoleBindings.") 110 | } 111 | 112 | for _, crb := range clusterRoleBindingsToDelete { 113 | r.Logger.Info("Deleting ClusterRoleBinding", "Name", crb.Name) 114 | err = r.Client.RbacV1().ClusterRoleBindings().Delete(context.Background(), crb.Name, metav1.DeleteOptions{}) 115 | 116 | if err != nil { 117 | r.Logger.WithValues("ClusterRoleBinding", crb.Name).Error(err, "Failed to delete ClusterRoleBinding.") 118 | } 119 | } 120 | } 121 | 122 | // CreateOrRecreateRoleBinding creates a new or recreates a existing RoleBinding 123 | func (r *Reconciler) CreateOrRecreateRoleBinding(rb rbacv1.RoleBinding) (*rbacv1.RoleBinding, error) { 124 | existing, err := r.Client.RbacV1().RoleBindings(rb.Namespace).Get(context.Background(), rb.Name, metav1.GetOptions{}) 125 | if err == nil { 126 | if !HasNamedOwner(existing.OwnerReferences, rbacName, "") { 127 | r.Logger.Info("Existing RoleBinding is not owned by a RbacDefinition. Ignoring", "Name", fmt.Sprintf("%s/%s", existing.Namespace, existing.Name)) 128 | return existing, nil 129 | } 130 | 131 | if util.IsRoleBindingEqual(*existing, rb) { 132 | return existing, nil 133 | } 134 | 135 | r.Logger.Info("Deleting RoleBinding", "Name", fmt.Sprintf("%s/%s", rb.Namespace, rb.Name)) 136 | err = r.Client.RbacV1().RoleBindings(rb.Namespace).Delete(context.Background(), rb.Name, metav1.DeleteOptions{}) 137 | if err != nil { 138 | return nil, err 139 | } 140 | } else if err != nil && !errors.IsNotFound(err) { 141 | return nil, err 142 | } 143 | 144 | r.Logger.Info("Creating new RoleBinding", "Name", fmt.Sprintf("%s/%s", rb.Namespace, rb.Name)) 145 | return r.Client.RbacV1().RoleBindings(rb.Namespace).Create(context.Background(), &rb, metav1.CreateOptions{}) 146 | } 147 | 148 | // CreateOrRecreateClusterRoleBinding creates a new or recreates a existing ClusterRoleBinding 149 | func (r *Reconciler) CreateOrRecreateClusterRoleBinding(crb rbacv1.ClusterRoleBinding) (*rbacv1.ClusterRoleBinding, error) { 150 | existing, err := r.Client.RbacV1().ClusterRoleBindings().Get(context.Background(), crb.Name, metav1.GetOptions{}) 151 | if err == nil { 152 | if !HasNamedOwner(existing.OwnerReferences, rbacName, "") { 153 | r.Logger.Info("Existing ClusterRoleBinding is not owned by a RbacDefinition. Ignoring", "Name", existing.Name) 154 | return existing, nil 155 | } 156 | 157 | if util.IsClusterRoleBindingEqual(*existing, crb) { 158 | return existing, nil 159 | } 160 | 161 | r.Logger.Info("Deleting ClusterRoleBinding", "Name", crb.Name) 162 | err = r.Client.RbacV1().ClusterRoleBindings().Delete(context.Background(), crb.Name, metav1.DeleteOptions{}) 163 | if err != nil { 164 | return nil, err 165 | } 166 | } else if err != nil && !errors.IsNotFound(err) { 167 | return nil, err 168 | } 169 | 170 | r.Logger.Info("Creating new ClusterRoleBinding", "Name", crb.Name) 171 | return r.Client.RbacV1().ClusterRoleBindings().Create(context.Background(), &crb, metav1.CreateOptions{}) 172 | } 173 | 174 | // BuildAllRoleBindings returns an array of RoleBindings for the given RbacDefinition 175 | func (r *Reconciler) BuildAllRoleBindings(cr *v1beta1.RbacDefinition) []rbacv1.RoleBinding { 176 | var bindingObjects []rbacv1.RoleBinding = []rbacv1.RoleBinding{} 177 | 178 | for _, nsSpec := range cr.Spec.Namespaced { 179 | relevantNamespaces := r.GetRelevantNamespaces(nsSpec.NamespaceSelector, nsSpec.Namespace) 180 | if relevantNamespaces == nil { 181 | return nil 182 | } 183 | 184 | r.Logger.WithValues("Namespaces", util.MapNamespaces(relevantNamespaces)).Info("Found matching Namespaces.") 185 | 186 | for _, ns := range relevantNamespaces { 187 | for _, bindingSpec := range nsSpec.Bindings { 188 | name := bindingSpec.Name 189 | 190 | if name == "" { 191 | name = bindingSpec.RoleName 192 | } 193 | 194 | subjects := bindingSpec.Subjects 195 | 196 | if bindingSpec.AllServiceAccounts { 197 | subjects = r.appendServiceAccountSubjects(r.getServiceAccounts(ns.Name), subjects) 198 | } 199 | 200 | if len(subjects) == 0 { 201 | continue 202 | } 203 | 204 | roleBinding := rbacv1.RoleBinding{ 205 | ObjectMeta: metav1.ObjectMeta{ 206 | Name: name, 207 | Namespace: ns.Name, 208 | }, 209 | RoleRef: rbacv1.RoleRef{ 210 | Name: bindingSpec.RoleName, 211 | Kind: bindingSpec.Kind, 212 | }, 213 | Subjects: subjects, 214 | } 215 | 216 | bindingObjects = append(bindingObjects, roleBinding) 217 | } 218 | } 219 | } 220 | 221 | return bindingObjects 222 | } 223 | 224 | // BuildAllClusterRoleBindings returns an array of ClusterRoleBindings for the given RbacDefinition 225 | func (r *Reconciler) BuildAllClusterRoleBindings(cr *v1beta1.RbacDefinition) []rbacv1.ClusterRoleBinding { 226 | var bindingObjects []rbacv1.ClusterRoleBinding = []rbacv1.ClusterRoleBinding{} 227 | 228 | for _, bindingSpec := range cr.Spec.Cluster { 229 | name := bindingSpec.Name 230 | 231 | if name == "" { 232 | name = bindingSpec.ClusterRoleName 233 | } 234 | 235 | if len(bindingSpec.Subjects) == 0 { 236 | continue 237 | } 238 | 239 | clusterRoleBinding := rbacv1.ClusterRoleBinding{ 240 | ObjectMeta: metav1.ObjectMeta{ 241 | Name: name, 242 | }, 243 | RoleRef: rbacv1.RoleRef{ 244 | Name: bindingSpec.ClusterRoleName, 245 | Kind: "ClusterRole", 246 | }, 247 | Subjects: bindingSpec.Subjects, 248 | } 249 | 250 | bindingObjects = append(bindingObjects, clusterRoleBinding) 251 | } 252 | 253 | return bindingObjects 254 | } 255 | 256 | // DeleteOwnedRoleBindings deletes all RoleBindings in namespace owned by the RbacDefinition 257 | func (r *Reconciler) DeleteOwnedRoleBindings(namespace string, def v1beta1.RbacDefinition) error { 258 | list, err := r.Client.RbacV1().RoleBindings(namespace).List(context.Background(), metav1.ListOptions{}) 259 | 260 | if err != nil { 261 | return err 262 | } 263 | 264 | for _, rb := range list.Items { 265 | for _, ref := range rb.OwnerReferences { 266 | if HasNamedOwner([]metav1.OwnerReference{ref}, rbacName, def.Name) { 267 | r.Logger.Info("Deleting owned RoleBinding", "Name", fmt.Sprintf("%s/%s", rb.Namespace, rb.Name)) 268 | err = r.Client.RbacV1().RoleBindings(namespace).Delete(context.Background(), rb.Name, metav1.DeleteOptions{}) 269 | 270 | if err != nil { 271 | return err 272 | } 273 | } 274 | } 275 | } 276 | 277 | return nil 278 | } 279 | 280 | func (r *Reconciler) getServiceAccounts(ns string) []corev1.ServiceAccount { 281 | accountList, err := r.Client.CoreV1().ServiceAccounts(ns).List(context.Background(), metav1.ListOptions{}) 282 | if err != nil { 283 | r.Logger.WithValues("NsName", ns).Error(err, "Could not list ServiceAccounts in namespace.") 284 | return nil 285 | } 286 | 287 | return accountList.Items 288 | } 289 | 290 | func (r *Reconciler) appendServiceAccountSubjects(accounts []corev1.ServiceAccount, subjects []rbacv1.Subject) []rbacv1.Subject { 291 | for _, account := range accounts { 292 | subject := rbacv1.Subject{ 293 | Kind: "ServiceAccount", 294 | Name: account.Name, 295 | } 296 | 297 | if !util.ContainsSubject(subjects, subject) { 298 | subjects = append(subjects, subject) 299 | } 300 | } 301 | 302 | return subjects 303 | } 304 | 305 | func (r *Reconciler) getRoleBindingsToDelete(defName string, creating []rbacv1.RoleBinding) ([]rbacv1.RoleBinding, error) { 306 | list, err := r.Client.RbacV1().RoleBindings("").List(context.Background(), metav1.ListOptions{}) 307 | 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | var bindings []rbacv1.RoleBinding = []rbacv1.RoleBinding{} 313 | 314 | for _, rb := range list.Items { 315 | if HasNamedOwner(rb.OwnerReferences, rbacName, defName) && !util.ContainsRoleBinding(creating, rb.Name, rb.Namespace) { 316 | bindings = append(bindings, rb) 317 | } 318 | } 319 | 320 | return bindings, nil 321 | } 322 | 323 | func (r *Reconciler) getClusterRoleBindingsToDelete(defName string, creating []rbacv1.ClusterRoleBinding) ([]rbacv1.ClusterRoleBinding, error) { 324 | list, err := r.Client.RbacV1().ClusterRoleBindings().List(context.Background(), metav1.ListOptions{}) 325 | 326 | if err != nil { 327 | return nil, err 328 | } 329 | 330 | var bindings []rbacv1.ClusterRoleBinding = []rbacv1.ClusterRoleBinding{} 331 | 332 | for _, crb := range list.Items { 333 | if HasNamedOwner(crb.OwnerReferences, rbacName, defName) && !util.ContainsClusterRoleBinding(creating, crb.Name) { 334 | bindings = append(bindings, crb) 335 | } 336 | } 337 | 338 | return bindings, nil 339 | } 340 | 341 | // IsServiceAccountRelevant checks if the given definition includes all serviceaccounts 342 | func (r *Reconciler) IsServiceAccountRelevant(spec v1beta1.RbacDefinition, ns string) bool { 343 | for _, nsSpec := range spec.Spec.Namespaced { 344 | namespaces := r.GetRelevantNamespaces(nsSpec.NamespaceSelector, nsSpec.Namespace) 345 | 346 | if util.ContainsNamespace(namespaces, ns) { 347 | for _, bindingSpec := range nsSpec.Bindings { 348 | if bindingSpec.AllServiceAccounts { 349 | return true 350 | } 351 | } 352 | } 353 | } 354 | 355 | return false 356 | } 357 | -------------------------------------------------------------------------------- /pkg/reconciler/reconciler_test.go: -------------------------------------------------------------------------------- 1 | package reconciler_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync/atomic" 7 | "testing" 8 | 9 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 10 | "github.com/ckotzbauer/access-manager/pkg/reconciler" 11 | "github.com/ckotzbauer/access-manager/pkg/util" 12 | 13 | b64 "encoding/base64" 14 | 15 | "github.com/go-logr/logr" 16 | . "github.com/onsi/ginkgo" 17 | . "github.com/onsi/gomega" 18 | corev1 "k8s.io/api/core/v1" 19 | rbacv1 "k8s.io/api/rbac/v1" 20 | "k8s.io/apimachinery/pkg/api/errors" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/client-go/kubernetes" 24 | kscheme "k8s.io/client-go/kubernetes/scheme" 25 | "sigs.k8s.io/controller-runtime/pkg/envtest" 26 | "sigs.k8s.io/controller-runtime/pkg/log" 27 | logf "sigs.k8s.io/controller-runtime/pkg/log" 28 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 29 | ) 30 | 31 | func TestReconciliation(t *testing.T) { 32 | RegisterFailHandler(Fail) 33 | RunSpecs(t, "Reconciliation Suite") 34 | } 35 | 36 | var testenv *envtest.Environment 37 | var clientset *kubernetes.Clientset 38 | 39 | var _ = BeforeSuite(func(done Done) { 40 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter))) 41 | 42 | testenv = &envtest.Environment{} 43 | 44 | var err error 45 | cfg, err := testenv.Start() 46 | Expect(err).NotTo(HaveOccurred()) 47 | 48 | clientset, err = kubernetes.NewForConfig(cfg) 49 | Expect(err).NotTo(HaveOccurred()) 50 | Expect(clientset).NotTo(BeNil()) 51 | 52 | close(done) 53 | }, 60) 54 | 55 | var _ = AfterSuite(func() { 56 | Expect(testenv.Stop()).To(Succeed()) 57 | }) 58 | 59 | func createNamespaces(ctx context.Context, nss ...*corev1.Namespace) { 60 | for _, ns := range nss { 61 | _, err := clientset.CoreV1().Namespaces().Get(ctx, ns.Name, metav1.GetOptions{}) 62 | if err != nil && errors.IsNotFound(err) { 63 | ns, err = clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) 64 | Expect(err).NotTo(HaveOccurred()) 65 | } 66 | } 67 | } 68 | 69 | func createServiceAccounts(ctx context.Context, accounts ...*corev1.ServiceAccount) { 70 | for _, account := range accounts { 71 | _, err := clientset.CoreV1().ServiceAccounts(account.Namespace).Get(ctx, account.Name, metav1.GetOptions{}) 72 | if err != nil && errors.IsNotFound(err) { 73 | account, err = clientset.CoreV1().ServiceAccounts(account.Namespace).Create(ctx, account, metav1.CreateOptions{}) 74 | Expect(err).NotTo(HaveOccurred()) 75 | } 76 | } 77 | } 78 | 79 | func createClusterRoleBindings(ctx context.Context, crbs ...*rbacv1.ClusterRoleBinding) { 80 | for _, crb := range crbs { 81 | _, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 82 | if err != nil && errors.IsNotFound(err) { 83 | crb, err = clientset.RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{}) 84 | Expect(err).NotTo(HaveOccurred()) 85 | } 86 | } 87 | } 88 | 89 | func createRoleBindings(ctx context.Context, rbs ...*rbacv1.RoleBinding) { 90 | for _, rb := range rbs { 91 | _, err := clientset.RbacV1().RoleBindings(rb.Namespace).Get(ctx, rb.Name, metav1.GetOptions{}) 92 | if err != nil && errors.IsNotFound(err) { 93 | rb, err = clientset.RbacV1().RoleBindings(rb.Namespace).Create(ctx, rb, metav1.CreateOptions{}) 94 | Expect(err).NotTo(HaveOccurred()) 95 | } 96 | } 97 | } 98 | 99 | func createSecrets(ctx context.Context, secrets ...*corev1.Secret) { 100 | for _, secret := range secrets { 101 | _, err := clientset.CoreV1().Secrets(secret.Namespace).Get(ctx, secret.Name, metav1.GetOptions{}) 102 | if err != nil && errors.IsNotFound(err) { 103 | secret, err = clientset.CoreV1().Secrets(secret.Namespace).Create(ctx, secret, metav1.CreateOptions{}) 104 | Expect(err).NotTo(HaveOccurred()) 105 | } 106 | } 107 | } 108 | 109 | var _ = Describe("Reconciler", func() { 110 | var namespace1 *corev1.Namespace 111 | var namespace2 *corev1.Namespace 112 | var namespace3 *corev1.Namespace 113 | var namespace4 *corev1.Namespace 114 | var roleBinding1 *rbacv1.RoleBinding 115 | var secret1 *corev1.Secret 116 | var secret2 *corev1.Secret 117 | var count uint64 = 0 118 | var scheme *runtime.Scheme 119 | var logger logr.Logger 120 | var rec *reconciler.Reconciler 121 | ctx := context.Background() 122 | flag := true 123 | 124 | BeforeEach(func(done Done) { 125 | atomic.AddUint64(&count, 1) 126 | namespace1 = &corev1.Namespace{ 127 | ObjectMeta: metav1.ObjectMeta{ 128 | Name: fmt.Sprintf("ns-one-%v", count), 129 | Labels: map[string]string{"team": fmt.Sprintf("one-%v", count)}, 130 | }, 131 | Spec: corev1.NamespaceSpec{}, 132 | } 133 | namespace2 = &corev1.Namespace{ 134 | ObjectMeta: metav1.ObjectMeta{ 135 | Name: fmt.Sprintf("ns-two-%v", count), 136 | Labels: map[string]string{"team": fmt.Sprintf("two-%v", count), "ci": fmt.Sprintf("true-%v", count)}, 137 | }, 138 | Spec: corev1.NamespaceSpec{}, 139 | } 140 | namespace3 = &corev1.Namespace{ 141 | ObjectMeta: metav1.ObjectMeta{ 142 | Name: fmt.Sprintf("ns-three-%v", count), 143 | Labels: map[string]string{"team": fmt.Sprintf("three-%v", count), "ci": fmt.Sprintf("true-%v", count)}, 144 | }, 145 | Spec: corev1.NamespaceSpec{}, 146 | } 147 | namespace4 = &corev1.Namespace{ 148 | ObjectMeta: metav1.ObjectMeta{ 149 | Name: fmt.Sprintf("ns-four-%v", count), 150 | Labels: map[string]string{"team": fmt.Sprintf("four-%v", count)}, 151 | }, 152 | Spec: corev1.NamespaceSpec{}, 153 | } 154 | clusterRoleBinding1 := rbacv1.ClusterRoleBinding{ 155 | ObjectMeta: metav1.ObjectMeta{ 156 | Name: fmt.Sprintf("existing-crb1-%v", count), 157 | OwnerReferences: []metav1.OwnerReference{{Kind: "RbacDefinition", APIVersion: "access-manager.io/v1beta1", Controller: &flag, Name: "xx", UID: "123456"}}, 158 | }, 159 | RoleRef: rbacv1.RoleRef{ 160 | Name: "test-role", 161 | Kind: "ClusterRole", 162 | }, 163 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 164 | } 165 | clusterRoleBinding2 := rbacv1.ClusterRoleBinding{ 166 | ObjectMeta: metav1.ObjectMeta{ 167 | Name: fmt.Sprintf("existing-crb2-%v", count), 168 | }, 169 | RoleRef: rbacv1.RoleRef{ 170 | Name: "test-role", 171 | Kind: "ClusterRole", 172 | }, 173 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 174 | } 175 | roleBinding1 = &rbacv1.RoleBinding{ 176 | ObjectMeta: metav1.ObjectMeta{ 177 | Name: fmt.Sprintf("existing-rb1-%v", count), 178 | Namespace: "default", 179 | OwnerReferences: []metav1.OwnerReference{{Kind: "RbacDefinition", APIVersion: "access-manager.io/v1beta1", Controller: &flag, Name: "xx", UID: "123456"}}, 180 | }, 181 | RoleRef: rbacv1.RoleRef{ 182 | Name: "test-role", 183 | Kind: "ClusterRole", 184 | }, 185 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 186 | } 187 | roleBinding2 := &rbacv1.RoleBinding{ 188 | ObjectMeta: metav1.ObjectMeta{ 189 | Name: fmt.Sprintf("existing-rb2-%v", count), 190 | Namespace: "default", 191 | }, 192 | RoleRef: rbacv1.RoleRef{ 193 | Name: "test-role", 194 | Kind: "ClusterRole", 195 | }, 196 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 197 | } 198 | serviceAccount1 := &corev1.ServiceAccount{ 199 | ObjectMeta: metav1.ObjectMeta{ 200 | Name: fmt.Sprintf("one-%v", count), 201 | Namespace: fmt.Sprintf("ns-four-%v", count), 202 | }, 203 | } 204 | serviceAccount2 := &corev1.ServiceAccount{ 205 | ObjectMeta: metav1.ObjectMeta{ 206 | Name: fmt.Sprintf("two-%v", count), 207 | Namespace: fmt.Sprintf("ns-four-%v", count), 208 | }, 209 | } 210 | secret1 = &corev1.Secret{ 211 | ObjectMeta: metav1.ObjectMeta{ 212 | Name: fmt.Sprintf("source-secret1-%v", count), 213 | Namespace: "default", 214 | }, 215 | Type: corev1.SecretTypeOpaque, 216 | Data: map[string][]byte{"key1": []byte(b64.StdEncoding.EncodeToString([]byte("value1")))}, 217 | } 218 | secret2 = &corev1.Secret{ 219 | ObjectMeta: metav1.ObjectMeta{ 220 | Name: fmt.Sprintf("source-secret2-%v", count), 221 | Namespace: namespace1.Name, 222 | }, 223 | Type: corev1.SecretTypeOpaque, 224 | Data: map[string][]byte{"key2": []byte(b64.StdEncoding.EncodeToString([]byte("value2")))}, 225 | } 226 | 227 | scheme = kscheme.Scheme 228 | logger = log.Log.WithName("testLogger") 229 | rec = &reconciler.Reconciler{Client: *clientset, Scheme: scheme, Logger: logger} 230 | createNamespaces(ctx, namespace1, namespace2, namespace3, namespace4) 231 | createClusterRoleBindings(ctx, &clusterRoleBinding1, &clusterRoleBinding2) 232 | createRoleBindings(ctx, roleBinding1, roleBinding2) 233 | createServiceAccounts(ctx, serviceAccount1, serviceAccount2) 234 | createSecrets(ctx, secret1, secret2) 235 | close(done) 236 | }) 237 | 238 | AfterEach(func(done Done) { 239 | close(done) 240 | }) 241 | 242 | Describe("GetRelevantNamespaces", func() { 243 | It("should not match any namespace", func(done Done) { 244 | spec := &v1beta1.NamespacedSpec{NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"no": "match"}}} 245 | 246 | found := rec.GetRelevantNamespaces(spec.NamespaceSelector, spec.Namespace) 247 | Expect(found).NotTo(BeNil()) 248 | Expect(found).To(BeEmpty()) 249 | close(done) 250 | }) 251 | 252 | It("should match namespace1", func(done Done) { 253 | spec := &v1beta1.NamespacedSpec{ 254 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"team": fmt.Sprintf("one-%v", count)}}, 255 | } 256 | 257 | found := rec.GetRelevantNamespaces(spec.NamespaceSelector, spec.Namespace) 258 | Expect(found).NotTo(BeNil()) 259 | Expect(util.MapNamespaces(found)).To(BeEquivalentTo([]string{namespace1.Name})) 260 | close(done) 261 | }) 262 | 263 | It("should match namespace2 and namespace3", func(done Done) { 264 | spec := &v1beta1.NamespacedSpec{ 265 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"ci": fmt.Sprintf("true-%v", count)}}, 266 | } 267 | 268 | found := rec.GetRelevantNamespaces(spec.NamespaceSelector, spec.Namespace) 269 | Expect(found).NotTo(BeNil()) 270 | Expect(util.MapNamespaces(found)).To(BeEquivalentTo([]string{namespace3.Name, namespace2.Name})) 271 | close(done) 272 | }) 273 | }) 274 | 275 | Describe("BuildAllClusterRoleBindings", func() { 276 | It("should return empty array", func(done Done) { 277 | cr := &v1beta1.RbacDefinition{ 278 | Spec: v1beta1.RbacDefinitionSpec{ 279 | Cluster: []v1beta1.ClusterSpec{}, 280 | }, 281 | } 282 | 283 | clusterRoles := rec.BuildAllClusterRoleBindings(cr) 284 | Expect(clusterRoles).To(BeEmpty()) 285 | close(done) 286 | }) 287 | 288 | It("should return nothing if no subjects are provided", func(done Done) { 289 | cr := &v1beta1.RbacDefinition{ 290 | Spec: v1beta1.RbacDefinitionSpec{ 291 | Cluster: []v1beta1.ClusterSpec{ 292 | { 293 | ClusterRoleName: "test-role", 294 | Subjects: []rbacv1.Subject{}, 295 | }, 296 | }, 297 | }, 298 | } 299 | 300 | clusterRoles := rec.BuildAllClusterRoleBindings(cr) 301 | Expect(clusterRoles).To(BeEmpty()) 302 | close(done) 303 | }) 304 | 305 | It("should return correct ClusterRoleBindings", func(done Done) { 306 | cr := &v1beta1.RbacDefinition{ 307 | Spec: v1beta1.RbacDefinitionSpec{ 308 | Cluster: []v1beta1.ClusterSpec{ 309 | { 310 | ClusterRoleName: "test-role", 311 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}}, 312 | }, 313 | { 314 | Name: "my-awesome-clusterrolebinding", 315 | ClusterRoleName: "admin-role", 316 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "admins"}}, 317 | }, 318 | { 319 | ClusterRoleName: "john-role", 320 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 321 | }, 322 | }, 323 | }, 324 | } 325 | 326 | expectedBindings := []rbacv1.ClusterRoleBinding{ 327 | { 328 | ObjectMeta: metav1.ObjectMeta{Name: "test-role"}, 329 | RoleRef: rbacv1.RoleRef{ 330 | Name: "test-role", 331 | Kind: "ClusterRole", 332 | }, 333 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}}, 334 | }, 335 | { 336 | ObjectMeta: metav1.ObjectMeta{Name: "my-awesome-clusterrolebinding"}, 337 | RoleRef: rbacv1.RoleRef{ 338 | Name: "admin-role", 339 | Kind: "ClusterRole", 340 | }, 341 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "admins"}}, 342 | }, 343 | { 344 | ObjectMeta: metav1.ObjectMeta{Name: "john-role"}, 345 | RoleRef: rbacv1.RoleRef{ 346 | Name: "john-role", 347 | Kind: "ClusterRole", 348 | }, 349 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 350 | }, 351 | } 352 | 353 | clusterRoles := rec.BuildAllClusterRoleBindings(cr) 354 | Expect(clusterRoles).To(BeEquivalentTo(expectedBindings)) 355 | close(done) 356 | }) 357 | }) 358 | 359 | Describe("BuildAllRoleBindings", func() { 360 | It("should return empty array - no specs", func(done Done) { 361 | cr := &v1beta1.RbacDefinition{ 362 | Spec: v1beta1.RbacDefinitionSpec{ 363 | Namespaced: []v1beta1.NamespacedSpec{}, 364 | }, 365 | } 366 | 367 | roles := rec.BuildAllRoleBindings(cr) 368 | Expect(roles).NotTo(BeNil()) 369 | Expect(roles).To(BeEmpty()) 370 | close(done) 371 | }) 372 | 373 | It("should return empty array - no namespaces", func(done Done) { 374 | cr := &v1beta1.RbacDefinition{ 375 | Spec: v1beta1.RbacDefinitionSpec{ 376 | Namespaced: []v1beta1.NamespacedSpec{ 377 | { 378 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"not": "existent"}}, 379 | }, 380 | }, 381 | }, 382 | } 383 | 384 | roles := rec.BuildAllRoleBindings(cr) 385 | Expect(roles).NotTo(BeNil()) 386 | Expect(roles).To(BeEmpty()) 387 | close(done) 388 | }) 389 | 390 | It("should return empty array - no subjects", func(done Done) { 391 | cr := &v1beta1.RbacDefinition{ 392 | Spec: v1beta1.RbacDefinitionSpec{ 393 | Namespaced: []v1beta1.NamespacedSpec{ 394 | { 395 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"team": fmt.Sprintf("one-%v", count)}}, 396 | Bindings: []v1beta1.BindingsSpec{ 397 | { 398 | Kind: "ClusterRole", 399 | RoleName: "admin-role", 400 | Subjects: []rbacv1.Subject{}, 401 | }, 402 | }, 403 | }, 404 | }, 405 | }, 406 | } 407 | 408 | roles := rec.BuildAllRoleBindings(cr) 409 | Expect(roles).To(BeEmpty()) 410 | close(done) 411 | }) 412 | 413 | It("should return correct RoleBindings - one namespace", func(done Done) { 414 | cr := &v1beta1.RbacDefinition{ 415 | Spec: v1beta1.RbacDefinitionSpec{ 416 | Namespaced: []v1beta1.NamespacedSpec{ 417 | { 418 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"team": fmt.Sprintf("one-%v", count)}}, 419 | Bindings: []v1beta1.BindingsSpec{ 420 | { 421 | Kind: "ClusterRole", 422 | RoleName: "admin-role", 423 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 424 | }, 425 | { 426 | Kind: "Role", 427 | Name: "my-awesome-rolebinding", 428 | RoleName: "test-role", 429 | Subjects: []rbacv1.Subject{ 430 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}, 431 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "manager"}, 432 | }, 433 | }, 434 | }, 435 | }, 436 | }, 437 | }, 438 | } 439 | 440 | expectedBindings := []rbacv1.RoleBinding{ 441 | { 442 | ObjectMeta: metav1.ObjectMeta{Name: "admin-role", Namespace: namespace1.Name}, 443 | RoleRef: rbacv1.RoleRef{ 444 | Name: "admin-role", 445 | Kind: "ClusterRole", 446 | }, 447 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 448 | }, 449 | { 450 | ObjectMeta: metav1.ObjectMeta{Name: "my-awesome-rolebinding", Namespace: namespace1.Name}, 451 | RoleRef: rbacv1.RoleRef{ 452 | Name: "test-role", 453 | Kind: "Role", 454 | }, 455 | Subjects: []rbacv1.Subject{ 456 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}, 457 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "manager"}, 458 | }, 459 | }, 460 | } 461 | 462 | clusterRoles := rec.BuildAllRoleBindings(cr) 463 | Expect(clusterRoles).NotTo(BeNil()) 464 | Expect(clusterRoles).To(BeEquivalentTo(expectedBindings)) 465 | close(done) 466 | }) 467 | 468 | It("should return correct RoleBindings - multiple namespace", func(done Done) { 469 | cr := &v1beta1.RbacDefinition{ 470 | Spec: v1beta1.RbacDefinitionSpec{ 471 | Namespaced: []v1beta1.NamespacedSpec{ 472 | { 473 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"ci": fmt.Sprintf("true-%v", count)}}, 474 | Bindings: []v1beta1.BindingsSpec{ 475 | { 476 | Kind: "ClusterRole", 477 | RoleName: "reader-role", 478 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 479 | }, 480 | { 481 | Kind: "Role", 482 | RoleName: "ci-role", 483 | Subjects: []rbacv1.Subject{ 484 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}, 485 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "ci"}, 486 | }, 487 | }, 488 | }, 489 | }, 490 | }, 491 | }, 492 | } 493 | 494 | expectedBindings := []rbacv1.RoleBinding{ 495 | { 496 | ObjectMeta: metav1.ObjectMeta{Name: "reader-role", Namespace: namespace3.Name}, 497 | RoleRef: rbacv1.RoleRef{ 498 | Name: "reader-role", 499 | Kind: "ClusterRole", 500 | }, 501 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 502 | }, 503 | { 504 | ObjectMeta: metav1.ObjectMeta{Name: "ci-role", Namespace: namespace3.Name}, 505 | RoleRef: rbacv1.RoleRef{ 506 | Name: "ci-role", 507 | Kind: "Role", 508 | }, 509 | Subjects: []rbacv1.Subject{ 510 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}, 511 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "ci"}, 512 | }, 513 | }, 514 | { 515 | ObjectMeta: metav1.ObjectMeta{Name: "reader-role", Namespace: namespace2.Name}, 516 | RoleRef: rbacv1.RoleRef{ 517 | Name: "reader-role", 518 | Kind: "ClusterRole", 519 | }, 520 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 521 | }, 522 | { 523 | ObjectMeta: metav1.ObjectMeta{Name: "ci-role", Namespace: namespace2.Name}, 524 | RoleRef: rbacv1.RoleRef{ 525 | Name: "ci-role", 526 | Kind: "Role", 527 | }, 528 | Subjects: []rbacv1.Subject{ 529 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "default"}, 530 | {APIGroup: "rbac.authorization.k8s.io", Kind: "ServiceAccount", Name: "ci"}, 531 | }, 532 | }, 533 | } 534 | 535 | clusterRoles := rec.BuildAllRoleBindings(cr) 536 | Expect(clusterRoles).NotTo(BeNil()) 537 | Expect(clusterRoles).To(BeEquivalentTo(expectedBindings)) 538 | close(done) 539 | }) 540 | 541 | It("should return correct RoleBindings - allServiceAccounts 1", func(done Done) { 542 | cr := &v1beta1.RbacDefinition{ 543 | Spec: v1beta1.RbacDefinitionSpec{ 544 | Namespaced: []v1beta1.NamespacedSpec{ 545 | { 546 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"team": fmt.Sprintf("four-%v", count)}}, 547 | Bindings: []v1beta1.BindingsSpec{ 548 | { 549 | Kind: "Role", 550 | Name: "my-awesome-rolebinding", 551 | RoleName: "test-role", 552 | AllServiceAccounts: true, 553 | }, 554 | }, 555 | }, 556 | }, 557 | }, 558 | } 559 | 560 | expectedBindings := []rbacv1.RoleBinding{ 561 | { 562 | ObjectMeta: metav1.ObjectMeta{Name: "my-awesome-rolebinding", Namespace: namespace4.Name}, 563 | RoleRef: rbacv1.RoleRef{ 564 | Name: "test-role", 565 | Kind: "Role", 566 | }, 567 | Subjects: []rbacv1.Subject{ 568 | {APIGroup: "", Kind: "ServiceAccount", Name: fmt.Sprintf("one-%v", count)}, 569 | {APIGroup: "", Kind: "ServiceAccount", Name: fmt.Sprintf("two-%v", count)}, 570 | }, 571 | }, 572 | } 573 | 574 | clusterRoles := rec.BuildAllRoleBindings(cr) 575 | Expect(clusterRoles).NotTo(BeNil()) 576 | Expect(clusterRoles).To(BeEquivalentTo(expectedBindings)) 577 | close(done) 578 | }) 579 | 580 | It("should return correct RoleBindings - allServiceAccounts 2", func(done Done) { 581 | cr := &v1beta1.RbacDefinition{ 582 | Spec: v1beta1.RbacDefinitionSpec{ 583 | Namespaced: []v1beta1.NamespacedSpec{ 584 | { 585 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"team": fmt.Sprintf("four-%v", count)}}, 586 | Bindings: []v1beta1.BindingsSpec{ 587 | { 588 | Kind: "Role", 589 | Name: "my-awesome-rolebinding", 590 | RoleName: "test-role", 591 | AllServiceAccounts: true, 592 | Subjects: []rbacv1.Subject{ 593 | {APIGroup: "", Kind: "ServiceAccount", Name: fmt.Sprintf("one-%v", count)}, 594 | {APIGroup: "", Kind: "ServiceAccount", Name: "myacc"}, 595 | }, 596 | }, 597 | }, 598 | }, 599 | }, 600 | }, 601 | } 602 | 603 | expectedBindings := []rbacv1.RoleBinding{ 604 | { 605 | ObjectMeta: metav1.ObjectMeta{Name: "my-awesome-rolebinding", Namespace: namespace4.Name}, 606 | RoleRef: rbacv1.RoleRef{ 607 | Name: "test-role", 608 | Kind: "Role", 609 | }, 610 | Subjects: []rbacv1.Subject{ 611 | {APIGroup: "", Kind: "ServiceAccount", Name: fmt.Sprintf("one-%v", count)}, 612 | {APIGroup: "", Kind: "ServiceAccount", Name: "myacc"}, 613 | {APIGroup: "", Kind: "ServiceAccount", Name: fmt.Sprintf("two-%v", count)}, 614 | }, 615 | }, 616 | } 617 | 618 | clusterRoles := rec.BuildAllRoleBindings(cr) 619 | Expect(clusterRoles).NotTo(BeNil()) 620 | Expect(clusterRoles).To(BeEquivalentTo(expectedBindings)) 621 | close(done) 622 | }) 623 | }) 624 | 625 | Describe("CreateOrRecreateClusterRoleBinding", func() { 626 | It("should create a new ClusterRoleBinding", func(done Done) { 627 | crb := rbacv1.ClusterRoleBinding{ 628 | ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("test-crb-%v", count)}, 629 | RoleRef: rbacv1.RoleRef{ 630 | Name: "test-role", 631 | Kind: "ClusterRole", 632 | }, 633 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 634 | } 635 | 636 | _, err := rec.CreateOrRecreateClusterRoleBinding(crb) 637 | Expect(err).NotTo(HaveOccurred()) 638 | 639 | _, err = clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 640 | Expect(err).NotTo(HaveOccurred()) 641 | close(done) 642 | }) 643 | 644 | It("should recreate a existing ClusterRoleBinding", func(done Done) { 645 | crb := rbacv1.ClusterRoleBinding{ 646 | ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("existing-crb1-%v", count)}, 647 | RoleRef: rbacv1.RoleRef{ 648 | Name: "new-role", 649 | Kind: "ClusterRole", 650 | }, 651 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "ci", Namespace: "default"}}, 652 | } 653 | 654 | _, err := rec.CreateOrRecreateClusterRoleBinding(crb) 655 | Expect(err).NotTo(HaveOccurred()) 656 | 657 | updated, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 658 | Expect(updated.RoleRef.Name == "new-role").To(BeTrue()) 659 | Expect(updated.Subjects[0].Name == "ci").To(BeTrue()) 660 | Expect(err).NotTo(HaveOccurred()) 661 | close(done) 662 | }) 663 | 664 | It("should not touch a unchanged ClusterRoleBinding", func(done Done) { 665 | original, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, fmt.Sprintf("existing-crb2-%v", count), metav1.GetOptions{}) 666 | Expect(err).NotTo(HaveOccurred()) 667 | 668 | crb := rbacv1.ClusterRoleBinding{ 669 | ObjectMeta: metav1.ObjectMeta{ 670 | Name: fmt.Sprintf("existing-crb2-%v", count), 671 | Namespace: "default", 672 | }, 673 | RoleRef: rbacv1.RoleRef{ 674 | Name: "test-role", 675 | Kind: "ClusterRole", 676 | }, 677 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 678 | } 679 | 680 | _, err = rec.CreateOrRecreateClusterRoleBinding(crb) 681 | Expect(err).NotTo(HaveOccurred()) 682 | 683 | unchanged, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 684 | Expect(unchanged.UID).To(BeEquivalentTo(original.UID)) 685 | Expect(err).NotTo(HaveOccurred()) 686 | close(done) 687 | }) 688 | }) 689 | 690 | Describe("CreateOrRecreateRoleBinding", func() { 691 | It("should create a new RoleBinding", func(done Done) { 692 | rb := rbacv1.RoleBinding{ 693 | ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("test-rb-%v", count), Namespace: "default"}, 694 | RoleRef: rbacv1.RoleRef{ 695 | Name: "test-role", 696 | Kind: "ClusterRole", 697 | }, 698 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 699 | } 700 | 701 | _, err := rec.CreateOrRecreateRoleBinding(rb) 702 | Expect(err).NotTo(HaveOccurred()) 703 | 704 | _, err = clientset.RbacV1().RoleBindings("default").Get(ctx, rb.Name, metav1.GetOptions{}) 705 | Expect(err).NotTo(HaveOccurred()) 706 | close(done) 707 | }) 708 | 709 | It("should recreate a existing RoleBinding", func(done Done) { 710 | original, err := clientset.RbacV1().RoleBindings("default").Get(ctx, fmt.Sprintf("existing-rb1-%v", count), metav1.GetOptions{}) 711 | Expect(err).NotTo(HaveOccurred()) 712 | 713 | rb := rbacv1.RoleBinding{ 714 | ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("existing-rb1-%v", count), Namespace: "default"}, 715 | RoleRef: rbacv1.RoleRef{ 716 | Name: "new-role", 717 | Kind: "ClusterRole", 718 | }, 719 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "ci", Namespace: "default"}}, 720 | } 721 | 722 | _, err = rec.CreateOrRecreateRoleBinding(rb) 723 | Expect(err).NotTo(HaveOccurred()) 724 | 725 | updated, err := clientset.RbacV1().RoleBindings("default").Get(ctx, rb.Name, metav1.GetOptions{}) 726 | Expect(updated.UID).ToNot(BeEquivalentTo(original.UID)) 727 | Expect(updated.RoleRef.Name == "new-role").To(BeTrue()) 728 | Expect(updated.Subjects[0].Name == "ci").To(BeTrue()) 729 | Expect(err).NotTo(HaveOccurred()) 730 | close(done) 731 | }) 732 | 733 | It("should not touch a unchanged RoleBinding", func(done Done) { 734 | original, err := clientset.RbacV1().RoleBindings("default").Get(ctx, fmt.Sprintf("existing-rb2-%v", count), metav1.GetOptions{}) 735 | Expect(err).NotTo(HaveOccurred()) 736 | 737 | rb := rbacv1.RoleBinding{ 738 | ObjectMeta: metav1.ObjectMeta{ 739 | Name: fmt.Sprintf("existing-rb2-%v", count), 740 | Namespace: "default", 741 | }, 742 | RoleRef: rbacv1.RoleRef{ 743 | Name: "test-role", 744 | Kind: "ClusterRole", 745 | }, 746 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 747 | } 748 | 749 | _, err = rec.CreateOrRecreateRoleBinding(rb) 750 | Expect(err).NotTo(HaveOccurred()) 751 | 752 | unchanged, err := clientset.RbacV1().RoleBindings("default").Get(ctx, rb.Name, metav1.GetOptions{}) 753 | Expect(unchanged.UID).To(BeEquivalentTo(original.UID)) 754 | Expect(err).NotTo(HaveOccurred()) 755 | close(done) 756 | }) 757 | }) 758 | 759 | Describe("RemoveAllDeletableRoleBindings", func() { 760 | It("should delete nothing", func(done Done) { 761 | rb := rbacv1.RoleBinding{ 762 | ObjectMeta: metav1.ObjectMeta{ 763 | Name: fmt.Sprintf("deletable-rb-%v", count), 764 | Namespace: namespace1.Name, 765 | OwnerReferences: []metav1.OwnerReference{{ 766 | Controller: &[]bool{true}[0], Kind: "RbacDefinition", Name: "john", APIVersion: "access-manager.io/v1beta1", UID: "12345", 767 | }}, 768 | }, 769 | RoleRef: rbacv1.RoleRef{ 770 | Name: "admin-role", 771 | Kind: "ClusterRole", 772 | }, 773 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 774 | } 775 | 776 | createRoleBindings(ctx, &rb) 777 | 778 | rec.RemoveAllDeletableRoleBindings("john", []rbacv1.RoleBinding{rb}) 779 | 780 | _, err := clientset.RbacV1().RoleBindings(namespace1.Name).Get(ctx, rb.Name, metav1.GetOptions{}) 781 | Expect(err).NotTo(HaveOccurred()) 782 | close(done) 783 | }) 784 | 785 | It("should delete rb", func(done Done) { 786 | rb := rbacv1.RoleBinding{ 787 | ObjectMeta: metav1.ObjectMeta{ 788 | Name: fmt.Sprintf("deletable-rb-%v", count), 789 | Namespace: namespace1.Name, 790 | OwnerReferences: []metav1.OwnerReference{{ 791 | Controller: &[]bool{true}[0], Kind: "RbacDefinition", Name: "john", APIVersion: "access-manager.io/v1beta1", UID: "12345", 792 | }}, 793 | }, 794 | RoleRef: rbacv1.RoleRef{ 795 | Name: "admin-role", 796 | Kind: "ClusterRole", 797 | }, 798 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 799 | } 800 | 801 | createRoleBindings(ctx, &rb) 802 | 803 | rec.RemoveAllDeletableRoleBindings("john", []rbacv1.RoleBinding{}) 804 | 805 | _, err := clientset.RbacV1().RoleBindings(namespace1.Name).Get(ctx, rb.Name, metav1.GetOptions{}) 806 | Expect(errors.IsNotFound(err)).To(BeTrue()) 807 | close(done) 808 | }) 809 | 810 | It("should not delete rb - other definition", func(done Done) { 811 | rb := rbacv1.RoleBinding{ 812 | ObjectMeta: metav1.ObjectMeta{ 813 | Name: fmt.Sprintf("deletable-rb-%v", count), 814 | Namespace: namespace1.Name, 815 | OwnerReferences: []metav1.OwnerReference{{ 816 | Controller: &[]bool{true}[0], Kind: "RbacDefinition", Name: "john", APIVersion: "access-manager.io/v1beta1", UID: "12345", 817 | }}, 818 | }, 819 | RoleRef: rbacv1.RoleRef{ 820 | Name: "admin-role", 821 | Kind: "ClusterRole", 822 | }, 823 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 824 | } 825 | 826 | createRoleBindings(ctx, &rb) 827 | 828 | rec.RemoveAllDeletableRoleBindings("joe", []rbacv1.RoleBinding{rb}) 829 | 830 | _, err := clientset.RbacV1().RoleBindings(namespace1.Name).Get(ctx, rb.Name, metav1.GetOptions{}) 831 | Expect(err).NotTo(HaveOccurred()) 832 | close(done) 833 | }) 834 | }) 835 | 836 | Describe("RemoveAllDeletableClusterRoleBindings", func() { 837 | It("should delete nothing", func(done Done) { 838 | crb := rbacv1.ClusterRoleBinding{ 839 | ObjectMeta: metav1.ObjectMeta{ 840 | Name: fmt.Sprintf("deletable-crb-%v", count), 841 | OwnerReferences: []metav1.OwnerReference{{ 842 | Controller: &[]bool{true}[0], Kind: "RbacDefinition", Name: "john", APIVersion: "access-manager.io/v1beta1", UID: "12345", 843 | }}, 844 | }, 845 | RoleRef: rbacv1.RoleRef{ 846 | Name: "admin-role", 847 | Kind: "ClusterRole", 848 | }, 849 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 850 | } 851 | 852 | createClusterRoleBindings(ctx, &crb) 853 | 854 | rec.RemoveAllDeletableClusterRoleBindings("john", []rbacv1.ClusterRoleBinding{crb}) 855 | 856 | _, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 857 | Expect(err).NotTo(HaveOccurred()) 858 | close(done) 859 | }) 860 | 861 | It("should delete rb", func(done Done) { 862 | crb := rbacv1.ClusterRoleBinding{ 863 | ObjectMeta: metav1.ObjectMeta{ 864 | Name: fmt.Sprintf("deletable-rb-%v", count), 865 | OwnerReferences: []metav1.OwnerReference{{ 866 | Controller: &[]bool{true}[0], Kind: "RbacDefinition", Name: "john", APIVersion: "access-manager.io/v1beta1", UID: "12345", 867 | }}, 868 | }, 869 | RoleRef: rbacv1.RoleRef{ 870 | Name: "admin-role", 871 | Kind: "ClusterRole", 872 | }, 873 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 874 | } 875 | 876 | createClusterRoleBindings(ctx, &crb) 877 | 878 | rec.RemoveAllDeletableClusterRoleBindings("john", []rbacv1.ClusterRoleBinding{}) 879 | 880 | _, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 881 | Expect(errors.IsNotFound(err)).To(BeTrue()) 882 | close(done) 883 | }) 884 | 885 | It("should not delete rb - other definition", func(done Done) { 886 | crb := rbacv1.ClusterRoleBinding{ 887 | ObjectMeta: metav1.ObjectMeta{ 888 | Name: fmt.Sprintf("deletable-rb-%v", count), 889 | OwnerReferences: []metav1.OwnerReference{{ 890 | Controller: &[]bool{true}[0], Kind: "RbacDefinition", Name: "john", APIVersion: "access-manager.io/v1beta1", UID: "12345", 891 | }}, 892 | }, 893 | RoleRef: rbacv1.RoleRef{ 894 | Name: "admin-role", 895 | Kind: "ClusterRole", 896 | }, 897 | Subjects: []rbacv1.Subject{{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "john"}}, 898 | } 899 | 900 | createClusterRoleBindings(ctx, &crb) 901 | 902 | rec.RemoveAllDeletableClusterRoleBindings("joe", []rbacv1.ClusterRoleBinding{crb}) 903 | 904 | _, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, crb.Name, metav1.GetOptions{}) 905 | Expect(err).NotTo(HaveOccurred()) 906 | close(done) 907 | }) 908 | }) 909 | 910 | Describe("DeleteOwnedRoleBindings", func() { 911 | It("should create a new RoleBinding", func(done Done) { 912 | flag := true 913 | 914 | ownedRb := rbacv1.RoleBinding{ 915 | ObjectMeta: metav1.ObjectMeta{ 916 | Name: fmt.Sprintf("owned-rb-%v", count), 917 | Namespace: "default", 918 | OwnerReferences: []metav1.OwnerReference{ 919 | { 920 | APIVersion: "access-manager.io/v1beta1", 921 | Controller: &flag, 922 | Kind: "RbacDefinition", 923 | Name: "test-def", 924 | UID: "123456", 925 | }, 926 | }, 927 | }, 928 | RoleRef: rbacv1.RoleRef{ 929 | Name: "test-role", 930 | Kind: "ClusterRole", 931 | }, 932 | Subjects: []rbacv1.Subject{{APIGroup: "", Kind: "ServiceAccount", Name: "default", Namespace: "default"}}, 933 | } 934 | 935 | def := &v1beta1.RbacDefinition{ 936 | ObjectMeta: metav1.ObjectMeta{Name: "test-def"}, 937 | Spec: v1beta1.RbacDefinitionSpec{ 938 | Namespaced: []v1beta1.NamespacedSpec{ 939 | { 940 | Namespace: v1beta1.NamespaceSpec{Name: "default"}, 941 | }, 942 | }, 943 | }, 944 | } 945 | 946 | createRoleBindings(ctx, &ownedRb) 947 | 948 | err := rec.DeleteOwnedRoleBindings("default", *def) 949 | Expect(err).NotTo(HaveOccurred()) 950 | 951 | _, err = clientset.RbacV1().RoleBindings("default").Get(ctx, ownedRb.Name, metav1.GetOptions{}) 952 | Expect(errors.IsNotFound(err)).To(BeTrue()) 953 | 954 | ex, err := clientset.RbacV1().RoleBindings("default").Get(ctx, roleBinding1.Name, metav1.GetOptions{}) 955 | Expect(err).NotTo(HaveOccurred()) 956 | Expect(ex).NotTo(BeNil()) 957 | 958 | close(done) 959 | }) 960 | }) 961 | 962 | Describe("IsServiceAccountRelevant", func() { 963 | It("should return true", func(done Done) { 964 | def := &v1beta1.RbacDefinition{ 965 | ObjectMeta: metav1.ObjectMeta{Name: "test-def"}, 966 | Spec: v1beta1.RbacDefinitionSpec{ 967 | Namespaced: []v1beta1.NamespacedSpec{ 968 | { 969 | Namespace: v1beta1.NamespaceSpec{Name: "default"}, 970 | Bindings: []v1beta1.BindingsSpec{{AllServiceAccounts: true}}, 971 | }, 972 | }, 973 | }, 974 | } 975 | 976 | result := rec.IsServiceAccountRelevant(*def, "default") 977 | Expect(result).To(BeTrue()) 978 | 979 | close(done) 980 | }) 981 | 982 | It("should return false - other namespace", func(done Done) { 983 | def := &v1beta1.RbacDefinition{ 984 | ObjectMeta: metav1.ObjectMeta{Name: "test-def"}, 985 | Spec: v1beta1.RbacDefinitionSpec{ 986 | Namespaced: []v1beta1.NamespacedSpec{ 987 | { 988 | Namespace: v1beta1.NamespaceSpec{Name: "default"}, 989 | Bindings: []v1beta1.BindingsSpec{{AllServiceAccounts: true}}, 990 | }, 991 | }, 992 | }, 993 | } 994 | 995 | result := rec.IsServiceAccountRelevant(*def, "namespace1") 996 | Expect(result).To(BeFalse()) 997 | 998 | close(done) 999 | }) 1000 | 1001 | It("should return false - not all sa's", func(done Done) { 1002 | def := &v1beta1.RbacDefinition{ 1003 | ObjectMeta: metav1.ObjectMeta{Name: "test-def"}, 1004 | Spec: v1beta1.RbacDefinitionSpec{ 1005 | Namespaced: []v1beta1.NamespacedSpec{ 1006 | { 1007 | Namespace: v1beta1.NamespaceSpec{Name: "default"}, 1008 | Bindings: []v1beta1.BindingsSpec{{AllServiceAccounts: false}}, 1009 | }, 1010 | }, 1011 | }, 1012 | } 1013 | 1014 | result := rec.IsServiceAccountRelevant(*def, "default") 1015 | Expect(result).To(BeFalse()) 1016 | 1017 | close(done) 1018 | }) 1019 | }) 1020 | 1021 | Describe("BuildAllSecrets", func() { 1022 | It("should return empty array - no source", func(done Done) { 1023 | cr := &v1beta1.SyncSecretDefinition{ 1024 | Spec: v1beta1.SyncSecretDefinitionSpec{ 1025 | Source: v1beta1.SourceSpec{}, 1026 | }, 1027 | } 1028 | 1029 | roles := rec.BuildAllSecrets(cr) 1030 | Expect(roles).NotTo(BeNil()) 1031 | Expect(roles).To(BeEmpty()) 1032 | close(done) 1033 | }) 1034 | 1035 | It("should return empty array - no target namespaces", func(done Done) { 1036 | cr := &v1beta1.SyncSecretDefinition{ 1037 | Spec: v1beta1.SyncSecretDefinitionSpec{ 1038 | Targets: []v1beta1.TargetSpec{ 1039 | { 1040 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"not": "existent"}}, 1041 | }, 1042 | }, 1043 | }, 1044 | } 1045 | 1046 | roles := rec.BuildAllSecrets(cr) 1047 | Expect(roles).NotTo(BeNil()) 1048 | Expect(roles).To(BeEmpty()) 1049 | close(done) 1050 | }) 1051 | 1052 | It("should return empty array - source secret does not exist", func(done Done) { 1053 | cr := &v1beta1.SyncSecretDefinition{ 1054 | Spec: v1beta1.SyncSecretDefinitionSpec{ 1055 | Source: v1beta1.SourceSpec{ 1056 | Name: "not-existing", 1057 | Namespace: "hello", 1058 | }, 1059 | }, 1060 | } 1061 | 1062 | roles := rec.BuildAllSecrets(cr) 1063 | Expect(roles).To(BeEmpty()) 1064 | close(done) 1065 | }) 1066 | 1067 | It("should return correct Secrets - one namespace", func(done Done) { 1068 | cr := &v1beta1.SyncSecretDefinition{ 1069 | Spec: v1beta1.SyncSecretDefinitionSpec{ 1070 | Source: v1beta1.SourceSpec{ 1071 | Name: secret1.Name, 1072 | Namespace: secret1.Namespace, 1073 | }, 1074 | Targets: []v1beta1.TargetSpec{ 1075 | { 1076 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"team": fmt.Sprintf("one-%v", count)}}, 1077 | }, 1078 | }, 1079 | }, 1080 | } 1081 | 1082 | expectedSecrets := []corev1.Secret{ 1083 | { 1084 | ObjectMeta: metav1.ObjectMeta{Name: secret1.Name, Namespace: namespace1.Name}, 1085 | Type: corev1.SecretTypeOpaque, 1086 | Data: secret1.Data, 1087 | }, 1088 | } 1089 | 1090 | secrets := rec.BuildAllSecrets(cr) 1091 | Expect(secrets).NotTo(BeNil()) 1092 | Expect(secrets).To(BeEquivalentTo(expectedSecrets)) 1093 | close(done) 1094 | }) 1095 | 1096 | It("should return correct Secrets - multiple namespace", func(done Done) { 1097 | cr := &v1beta1.SyncSecretDefinition{ 1098 | Spec: v1beta1.SyncSecretDefinitionSpec{ 1099 | Source: v1beta1.SourceSpec{ 1100 | Name: secret1.Name, 1101 | Namespace: secret1.Namespace, 1102 | }, 1103 | Targets: []v1beta1.TargetSpec{ 1104 | { 1105 | NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"ci": fmt.Sprintf("true-%v", count)}}, 1106 | }, 1107 | }, 1108 | }, 1109 | } 1110 | 1111 | expectedSecrets := []corev1.Secret{ 1112 | { 1113 | ObjectMeta: metav1.ObjectMeta{Name: secret1.Name, Namespace: namespace3.Name}, 1114 | Type: corev1.SecretTypeOpaque, 1115 | Data: secret1.Data, 1116 | }, 1117 | { 1118 | ObjectMeta: metav1.ObjectMeta{Name: secret1.Name, Namespace: namespace2.Name}, 1119 | Type: corev1.SecretTypeOpaque, 1120 | Data: secret1.Data, 1121 | }, 1122 | } 1123 | 1124 | secrets := rec.BuildAllSecrets(cr) 1125 | Expect(secrets).NotTo(BeNil()) 1126 | Expect(secrets).To(BeEquivalentTo(expectedSecrets)) 1127 | close(done) 1128 | }) 1129 | }) 1130 | 1131 | Describe("CreateSecret", func() { 1132 | It("should create a new Secret", func(done Done) { 1133 | secret := corev1.Secret{ 1134 | ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("test-secret-%v", count), Namespace: "default"}, 1135 | Type: corev1.SecretTypeOpaque, 1136 | StringData: map[string]string{"key10": "value10"}, 1137 | } 1138 | 1139 | _, err := rec.CreateSecret(secret) 1140 | Expect(err).NotTo(HaveOccurred()) 1141 | 1142 | _, err = clientset.CoreV1().Secrets("default").Get(ctx, secret.Name, metav1.GetOptions{}) 1143 | Expect(err).NotTo(HaveOccurred()) 1144 | close(done) 1145 | }) 1146 | 1147 | It("should not touch a Secret not owned", func(done Done) { 1148 | original, err := clientset.CoreV1().Secrets("default").Get(ctx, secret1.Name, metav1.GetOptions{}) 1149 | Expect(err).NotTo(HaveOccurred()) 1150 | 1151 | secret := corev1.Secret{ 1152 | ObjectMeta: metav1.ObjectMeta{ 1153 | Name: secret1.Name, 1154 | Namespace: "default", 1155 | }, 1156 | Type: corev1.SecretTypeOpaque, 1157 | Data: secret1.Data, 1158 | } 1159 | 1160 | _, err = rec.CreateSecret(secret) 1161 | Expect(err).NotTo(HaveOccurred()) 1162 | 1163 | unchanged, err := clientset.CoreV1().Secrets("default").Get(ctx, secret.Name, metav1.GetOptions{}) 1164 | Expect(unchanged.UID).To(BeEquivalentTo(original.UID)) 1165 | Expect(err).NotTo(HaveOccurred()) 1166 | close(done) 1167 | }) 1168 | }) 1169 | 1170 | Describe("RemoveOwnedSecrets", func() { 1171 | It("should create a new Secret", func(done Done) { 1172 | flag := true 1173 | 1174 | ownedSecret := corev1.Secret{ 1175 | ObjectMeta: metav1.ObjectMeta{ 1176 | Name: fmt.Sprintf("owned-secret-%v", count), 1177 | Namespace: "default", 1178 | OwnerReferences: []metav1.OwnerReference{ 1179 | { 1180 | APIVersion: "access-manager.io/v1beta1", 1181 | Controller: &flag, 1182 | Kind: "SyncSecretDefinition", 1183 | Name: "test-def", 1184 | UID: "123456", 1185 | }, 1186 | }, 1187 | }, 1188 | Type: corev1.SecretTypeOpaque, 1189 | StringData: map[string]string{"key5": "value5"}, 1190 | } 1191 | 1192 | def := &v1beta1.SyncSecretDefinition{ 1193 | ObjectMeta: metav1.ObjectMeta{Name: "test-def"}, 1194 | Spec: v1beta1.SyncSecretDefinitionSpec{ 1195 | Targets: []v1beta1.TargetSpec{ 1196 | { 1197 | Namespace: v1beta1.NamespaceSpec{Name: "default"}, 1198 | }, 1199 | }, 1200 | }, 1201 | } 1202 | 1203 | createSecrets(ctx, &ownedSecret) 1204 | owned, _ := rec.GetOwnedSecrets(def.Name) 1205 | rec.RemoveOwnedSecretsNotInList(owned, []corev1.Secret{}) 1206 | 1207 | _, err := clientset.CoreV1().Secrets("default").Get(ctx, ownedSecret.Name, metav1.GetOptions{}) 1208 | Expect(errors.IsNotFound(err)).To(BeTrue()) 1209 | 1210 | ex, err := clientset.CoreV1().Secrets("default").Get(ctx, secret1.Name, metav1.GetOptions{}) 1211 | Expect(err).NotTo(HaveOccurred()) 1212 | Expect(ex).NotTo(BeNil()) 1213 | 1214 | close(done) 1215 | }) 1216 | }) 1217 | }) 1218 | -------------------------------------------------------------------------------- /pkg/reconciler/secret_reconciler.go: -------------------------------------------------------------------------------- 1 | package reconciler 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | v1beta1 "github.com/ckotzbauer/access-manager/apis/access-manager.io/v1beta1" 9 | "github.com/ckotzbauer/access-manager/pkg/util" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | 15 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 16 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 17 | ) 18 | 19 | var secretName = "SyncSecretDefinition" 20 | 21 | // ReconcileSyncSecretDefinition applies all desired changes of the SyncSecretDefinition 22 | func (r *Reconciler) ReconcileSyncSecretDefinition(instance *v1beta1.SyncSecretDefinition) (reconcile.Result, error) { 23 | secrets := r.BuildAllSecrets(instance) 24 | ownedSecrets, err := r.GetOwnedSecrets(instance.Name) 25 | 26 | if err != nil { 27 | r.Logger.Error(err, "Failed to fetch all owned Secrets.") 28 | } 29 | 30 | r.RemoveOwnedSecretsNotInList(ownedSecrets, secrets) 31 | 32 | if err != nil { 33 | r.Logger.Error(err, "Failed to fetch owned Secrets.") 34 | return reconcile.Result{}, err 35 | } 36 | 37 | for _, s := range secrets { 38 | // Set SyncSecretDefinition instance as the owner and controller 39 | if err := controllerutil.SetControllerReference(instance, &s, r.Scheme); err != nil { 40 | r.Logger.WithValues("Secret", s.Namespace+"/"+s.Name).Error(err, "Failed to set controllerReference.") 41 | continue 42 | } 43 | 44 | existingSecret, err := r.getSecretFromSlice(ownedSecrets, s) 45 | 46 | if err == nil && r.hasSecretChanged(existingSecret, s) { 47 | r.Logger.Info("Reconciling Secret", "Name", fmt.Sprintf("%s/%s", existingSecret.Namespace, existingSecret.Name)) 48 | r.removeSecret(existingSecret) 49 | } else if err == nil { 50 | continue 51 | } else { 52 | r.Logger.Info("Reconciling Secret", "Name", fmt.Sprintf("%s/%s", s.Namespace, s.Name)) 53 | } 54 | 55 | if _, err := r.CreateSecret(s); err != nil { 56 | r.Logger.WithValues("Secret", s.Namespace+"/"+s.Name).Error(err, "Failed to reconcile Secret.") 57 | } 58 | } 59 | 60 | return reconcile.Result{}, nil 61 | } 62 | 63 | // ReconcileSecret applies all desired changes of the Secret 64 | func (r *Reconciler) ReconcileSecret(instance *corev1.Secret) (reconcile.Result, error) { 65 | list := &v1beta1.SyncSecretDefinitionList{} 66 | err := r.ControllerClient.List(context.Background(), list) 67 | 68 | if err != nil { 69 | r.Logger.Error(err, "Unexpected error occurred!") 70 | return reconcile.Result{}, err 71 | } 72 | 73 | for _, def := range list.Items { 74 | if def.Spec.Paused { 75 | continue 76 | } 77 | 78 | if !r.isSecretRelevant(def, instance) { 79 | continue 80 | } 81 | 82 | _, err = r.ReconcileSyncSecretDefinition(&def) 83 | 84 | if err != nil { 85 | return reconcile.Result{}, err 86 | } 87 | } 88 | 89 | return reconcile.Result{}, nil 90 | } 91 | 92 | // BuildAllSecrets returns an array of Secrets for the given SyncSecretDefinition 93 | func (r *Reconciler) BuildAllSecrets(cr *v1beta1.SyncSecretDefinition) []corev1.Secret { 94 | var secrets []corev1.Secret = []corev1.Secret{} 95 | sourceSecret, err := r.getSourceSecret(cr.Spec.Source.Name, cr.Spec.Source.Namespace) 96 | 97 | if err != nil { 98 | if errors.IsNotFound(err) { 99 | return secrets 100 | } 101 | 102 | r.Logger.WithValues("Secret", cr.Spec.Source.Namespace+"/"+cr.Spec.Source.Name).Error(err, "Failed to fetch source secret.") 103 | return secrets 104 | } 105 | 106 | for _, nsSpec := range cr.Spec.Targets { 107 | relevantNamespaces := r.GetRelevantNamespaces(nsSpec.NamespaceSelector, nsSpec.Namespace) 108 | if relevantNamespaces == nil { 109 | return nil 110 | } 111 | 112 | r.Logger.WithValues("Namespaces", util.MapNamespaces(relevantNamespaces)).Info("Found matching Namespaces.") 113 | 114 | for _, ns := range relevantNamespaces { 115 | secret := corev1.Secret{ 116 | ObjectMeta: metav1.ObjectMeta{ 117 | Name: sourceSecret.Name, 118 | Namespace: ns.Name, 119 | }, 120 | Type: sourceSecret.Type, 121 | Data: sourceSecret.Data, 122 | Immutable: sourceSecret.Immutable, 123 | } 124 | 125 | secrets = append(secrets, secret) 126 | } 127 | } 128 | 129 | return secrets 130 | } 131 | 132 | // CreateSecret creates a new Secret 133 | func (r *Reconciler) CreateSecret(s corev1.Secret) (*corev1.Secret, error) { 134 | existing, err := r.Client.CoreV1().Secrets(s.Namespace).Get(context.Background(), s.Name, metav1.GetOptions{}) 135 | if err == nil { 136 | if !HasNamedOwner(existing.OwnerReferences, secretName, "") { 137 | r.Logger.Info("Existing Secret is not owned by a SyncSecretDefinition. Ignoring", "Name", fmt.Sprintf("%s/%s", existing.Namespace, existing.Name)) 138 | return existing, nil 139 | } 140 | } else if err != nil && !errors.IsNotFound(err) { 141 | return nil, err 142 | } 143 | 144 | r.Logger.Info("Creating new Secret", "Name", fmt.Sprintf("%s/%s", s.Namespace, s.Name)) 145 | return r.Client.CoreV1().Secrets(s.Namespace).Create(context.Background(), &s, metav1.CreateOptions{}) 146 | } 147 | 148 | // RemoveOwnedSecretsNotInList deletes all secrets which are owned from the given object name and not in the slice. 149 | func (r *Reconciler) RemoveOwnedSecretsNotInList(ownedSecrets []corev1.Secret, secrets []corev1.Secret) { 150 | for _, s := range ownedSecrets { 151 | if !r.containsSecret(s, secrets) { 152 | r.removeSecret(s) 153 | } 154 | } 155 | } 156 | 157 | func (r *Reconciler) removeSecret(s corev1.Secret) { 158 | r.Logger.Info("Deleting Secret", "Name", fmt.Sprintf("%s/%s", s.Namespace, s.Name)) 159 | err := r.Client.CoreV1().Secrets(s.Namespace).Delete(context.Background(), s.Name, metav1.DeleteOptions{}) 160 | 161 | if err != nil { 162 | r.Logger.WithValues("Name", fmt.Sprintf("%s/%s", s.Namespace, s.Name)).Error(err, "Failed to delete Secret.") 163 | } 164 | } 165 | 166 | // GetOwnedSecrets returns a slice of all secrets which are owned by the given definition name. 167 | func (r *Reconciler) GetOwnedSecrets(defName string) ([]corev1.Secret, error) { 168 | list, err := r.Client.CoreV1().Secrets("").List(context.Background(), metav1.ListOptions{}) 169 | 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | var secrets []corev1.Secret = []corev1.Secret{} 175 | 176 | for _, s := range list.Items { 177 | if HasNamedOwner(s.OwnerReferences, secretName, defName) { 178 | secrets = append(secrets, s) 179 | } 180 | } 181 | 182 | return secrets, nil 183 | } 184 | 185 | func (r *Reconciler) getSourceSecret(name, ns string) (*corev1.Secret, error) { 186 | return r.Client.CoreV1().Secrets(ns).Get(context.Background(), name, metav1.GetOptions{}) 187 | } 188 | 189 | func (r *Reconciler) isSecretRelevant(spec v1beta1.SyncSecretDefinition, secret *corev1.Secret) bool { 190 | return spec.Spec.Source.Name == secret.Name && spec.Spec.Source.Namespace == secret.Namespace 191 | } 192 | 193 | func (r *Reconciler) getSecretFromSlice(secrets []corev1.Secret, secret corev1.Secret) (corev1.Secret, error) { 194 | for _, s := range secrets { 195 | if s.Namespace == secret.Namespace && s.Name == secret.Name { 196 | return s, nil 197 | } 198 | } 199 | 200 | return corev1.Secret{}, fmt.Errorf("no secret found") 201 | } 202 | 203 | func (r *Reconciler) hasSecretChanged(existingSecret corev1.Secret, secret corev1.Secret) bool { 204 | return existingSecret.Namespace != secret.Namespace || 205 | existingSecret.Name != secret.Name || 206 | existingSecret.Type != secret.Type || 207 | !reflect.DeepEqual(existingSecret.Data, secret.Data) 208 | } 209 | 210 | func (r *Reconciler) containsSecret(secret corev1.Secret, secrets []corev1.Secret) bool { 211 | for _, s := range secrets { 212 | if s.Namespace == secret.Namespace && s.Name == secret.Name { 213 | return true 214 | } 215 | } 216 | 217 | return false 218 | } 219 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | rbacv1 "k8s.io/api/rbac/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func MapNamespaces(vs []corev1.Namespace) []string { 12 | vsm := make([]string, len(vs)) 13 | for i, v := range vs { 14 | vsm[i] = mapNamespaceName(v) 15 | } 16 | return vsm 17 | } 18 | 19 | func mapNamespaceName(ns corev1.Namespace) string { 20 | return ns.Name 21 | } 22 | 23 | // ContainsSubject returns true, if the list of Subjects contains the given subject 24 | func ContainsSubject(subjects []rbacv1.Subject, subject rbacv1.Subject) bool { 25 | for _, s := range subjects { 26 | if reflect.DeepEqual(s, subject) { 27 | return true 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | // ContainsRoleBinding returns true, if the list of RBs contains a Binding with the specified name and namespace 35 | func ContainsRoleBinding(rbs []rbacv1.RoleBinding, name, ns string) bool { 36 | for _, rb := range rbs { 37 | if rb.Name == name && rb.Namespace == ns { 38 | return true 39 | } 40 | } 41 | 42 | return false 43 | } 44 | 45 | // ContainsClusterRoleBinding returns true, if the list of CRBs contains a Binding with the specified name 46 | func ContainsClusterRoleBinding(crbs []rbacv1.ClusterRoleBinding, name string) bool { 47 | for _, crb := range crbs { 48 | if crb.Name == name { 49 | return true 50 | } 51 | } 52 | 53 | return false 54 | } 55 | 56 | // ContainsNamespace returns true, if the list of namespaces contains a object with the specified name 57 | func ContainsNamespace(namespaces []corev1.Namespace, name string) bool { 58 | if namespaces == nil { 59 | return false 60 | } 61 | 62 | for _, ns := range namespaces { 63 | if ns.Name == name { 64 | return true 65 | } 66 | } 67 | 68 | return false 69 | } 70 | 71 | func namespacedName(meta metav1.ObjectMeta) string { 72 | return meta.Namespace + "/" + meta.Name 73 | } 74 | 75 | // IsRoleBindingEqual returns true if both objects are named equal in the same namespace and have the same RoleRef and Subjects 76 | func IsRoleBindingEqual(rb1 rbacv1.RoleBinding, rb2 rbacv1.RoleBinding) bool { 77 | rb1 = removeAPIGroupFromRoleBinding(rb1) 78 | name := namespacedName(rb1.ObjectMeta) == namespacedName(rb2.ObjectMeta) 79 | roleRef := reflect.DeepEqual(rb1.RoleRef, rb2.RoleRef) 80 | subjects := reflect.DeepEqual(rb1.Subjects, rb2.Subjects) 81 | return name && roleRef && subjects 82 | } 83 | 84 | // IsClusterRoleBindingEqual returns true if both objects are named equal and have the same RoleRef and Subjects 85 | func IsClusterRoleBindingEqual(crb1 rbacv1.ClusterRoleBinding, crb2 rbacv1.ClusterRoleBinding) bool { 86 | crb1 = removeAPIGroupFromClusterRoleBinding(crb1) 87 | name := crb1.Name == crb2.Name 88 | roleRef := reflect.DeepEqual(crb1.RoleRef, crb2.RoleRef) 89 | subjects := reflect.DeepEqual(crb1.Subjects, crb2.Subjects) 90 | return name && roleRef && subjects 91 | } 92 | 93 | func removeAPIGroupFromRoleBinding(rb rbacv1.RoleBinding) rbacv1.RoleBinding { 94 | rb.RoleRef.APIGroup = "" 95 | var subjects []rbacv1.Subject = []rbacv1.Subject{} 96 | 97 | for _, subject := range rb.Subjects { 98 | subject.APIGroup = "" 99 | subjects = append(subjects, subject) 100 | } 101 | 102 | rb.Subjects = subjects 103 | return rb 104 | } 105 | 106 | func removeAPIGroupFromClusterRoleBinding(crb rbacv1.ClusterRoleBinding) rbacv1.ClusterRoleBinding { 107 | crb.RoleRef.APIGroup = "" 108 | var subjects []rbacv1.Subject = []rbacv1.Subject{} 109 | 110 | for _, subject := range crb.Subjects { 111 | subject.APIGroup = "" 112 | subjects = append(subjects, subject) 113 | } 114 | 115 | crb.Subjects = subjects 116 | return crb 117 | } 118 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>ckotzbauer/renovate-config:default", 4 | "github>ckotzbauer/renovate-config:weekly" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchPackageNames": [ 9 | "k8s.io/api", 10 | "k8s.io/apimachinery", 11 | "k8s.io/client-go" 12 | ], 13 | "reviewers": [ 14 | "ckotzbauer" 15 | ], 16 | "groupName": [ 17 | "Kubernetes versions" 18 | ] 19 | } 20 | ] 21 | } --------------------------------------------------------------------------------