Repository: redhat-cop/operator-utils Branch: master Commit: 09e27b5626f9 Files: 121 Total size: 489.2 KB Directory structure: gitextract_fz5mf0kc/ ├── .github/ │ └── workflows/ │ ├── pr.yaml │ └── push.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api/ │ └── v1alpha1/ │ ├── enforcingcrd_types.go │ ├── enforcingpatch_types.go │ ├── enforcingreconcilerstatus.go │ ├── groupversion_info.go │ ├── lockedpatch.go │ ├── lockedresource.go │ ├── mycrd_types.go │ ├── templatedenforcingcrd_types.go │ └── zz_generated.deepcopy.go ├── ci.Dockerfile ├── config/ │ ├── certmanager/ │ │ ├── certificate.yaml │ │ ├── kustomization.yaml │ │ └── kustomizeconfig.yaml │ ├── crd/ │ │ ├── bases/ │ │ │ ├── operator-utils.example.io_enforcingcrds.yaml │ │ │ ├── operator-utils.example.io_enforcingpatches.yaml │ │ │ ├── operator-utils.example.io_mycrds.yaml │ │ │ └── operator-utils.example.io_templatedenforcingcrds.yaml │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── patches/ │ │ ├── cainjection_in_enforcingcrds.yaml │ │ ├── cainjection_in_enforcingpatches.yaml │ │ ├── cainjection_in_mycrds.yaml │ │ ├── cainjection_in_templatedenforcingcrds.yaml │ │ ├── webhook_in_enforcingcrds.yaml │ │ ├── webhook_in_enforcingpatches.yaml │ │ ├── webhook_in_mycrds.yaml │ │ └── webhook_in_templatedenforcingcrds.yaml │ ├── default/ │ │ ├── kustomization.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ ├── manager_webhook_patch.yaml │ │ └── webhookcainjection_patch.yaml │ ├── helmchart/ │ │ ├── .helmignore │ │ ├── Chart.yaml.tpl │ │ ├── kustomization.yaml │ │ ├── templates/ │ │ │ ├── _helpers.tpl │ │ │ └── manager.yaml │ │ └── values.yaml.tpl │ ├── local-development/ │ │ └── kustomization.yaml │ ├── manager/ │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── manifests/ │ │ ├── bases/ │ │ │ └── operator-utils.clusterserviceversion.yaml │ │ └── kustomization.yaml │ ├── prometheus/ │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── monitor.yaml │ ├── rbac/ │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── enforcingcrd_editor_role.yaml │ │ ├── enforcingcrd_viewer_role.yaml │ │ ├── enforcingpatch_editor_role.yaml │ │ ├── enforcingpatch_viewer_role.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── mycrd_editor_role.yaml │ │ ├── mycrd_viewer_role.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ ├── templatedenforcingcrd_editor_role.yaml │ │ └── templatedenforcingcrd_viewer_role.yaml │ ├── samples/ │ │ ├── kustomization.yaml │ │ ├── operator-utils_v1alpha1_enforcingcrd.yaml │ │ ├── operator-utils_v1alpha1_enforcingpatch.yaml │ │ ├── operator-utils_v1alpha1_mycrd.yaml │ │ └── operator-utils_v1alpha1_templatedenforcingcrd.yaml │ ├── scorecard/ │ │ ├── bases/ │ │ │ └── config.yaml │ │ ├── kustomization.yaml │ │ └── patches/ │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── webhook/ │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── service.yaml ├── controllers/ │ ├── enforcingcrd_controller.go │ ├── enforcingpatch_controller.go │ ├── mycrd_controller.go │ ├── suite_test.go │ └── templatedenforcingcrd_controller.go ├── go.mod ├── go.sum ├── hack/ │ └── boilerplate.go.txt ├── main.go ├── pkg/ │ └── util/ │ ├── apis/ │ │ ├── conditions.go │ │ └── key.go │ ├── crud/ │ │ └── crudutils.go │ ├── discoveryclient/ │ │ └── discoveryclientutils.go │ ├── dynamicclient/ │ │ └── dynamicclientutils.go │ ├── finalizer.go │ ├── lockedresourcecontroller/ │ │ ├── enforcing-reconciler.go │ │ ├── locked-resource-manager.go │ │ ├── lockedpatch/ │ │ │ └── lockedpatch.go │ │ ├── lockedresource/ │ │ │ ├── lockedresource.go │ │ │ ├── lockedresourceset/ │ │ │ │ ├── lockedresourceset.go │ │ │ │ ├── lockedresourceset_bench_test.go │ │ │ │ └── lockedresourceset_test.go │ │ │ └── patch.go │ │ ├── patch-reconciler.go │ │ └── resource-reconciler.go │ ├── owner.go │ ├── predicates.go │ ├── reconciler.go │ ├── stoppablemanager/ │ │ └── stoppable-manager.go │ └── templates/ │ ├── advanced-funcmap.go │ └── templates.go ├── test/ │ ├── enforcing-patch-multiple-cluster-level.yaml │ ├── enforcing-patch-multiple.yaml │ ├── enforcing-patch.yaml │ ├── enforcing_cr.yaml │ ├── failing-enforcing_cr.yaml │ ├── mycrd_cr.yaml │ └── templatedenforcing_cr.yaml └── testbin/ └── setup-envtest.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/pr.yaml ================================================ name: pull request on: pull_request: branches: - master - main jobs: setup: runs-on: ubuntu-latest name: setup env: BUILD_PLATFORMS: "linux/amd64,linux/arm64,linux/ppc64le,linux/s390x" GO_VERSION: "~1.21" steps: - name: Setting Workflow Variables id: set-variables run: | echo "::set-output name=repository_name::$(basename $GITHUB_REPOSITORY)" echo "::set-output name=bin_dir::$(pwd)/bin" # Create Distribution Matrix echo "::set-output name=dist_matrix::$(echo -n "${{ env.BUILD_PLATFORMS }}" | jq -csR '. | split(",")')" # Set versions based on presence of tag if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then TAG="${GITHUB_REF/refs\/tags\//}" echo "::set-output name=tag_event::true" echo "::set-output name=operator_version::$TAG" else echo "::set-output name=tag_event::false" echo "::set-output name=operator_version::$DEFAULT_OPERATOR_VERSION" fi - name: Build Go Cache Paths id: go-cache-paths run: | echo "::set-output name=go-build::$(go env GOCACHE)" echo "::set-output name=go-mod::$(go env GOMODCACHE)" - name: Set up Go 1.x uses: actions/setup-go@v1 with: go-version: ${{ inputs.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v2 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Go Dependencies run: go mod download - name: Download Binaries env: OPERATOR_SDK_VERSION: ${{ inputs.OPERATOR_SDK_VERSION }} run: | # Create Binary Directory mkdir -p ${{ steps.set-variables.outputs.bin_dir }} # Operator SDK curl -L -o ${{ steps.set-variables.outputs.bin_dir }}/operator-sdk https://github.com/operator-framework/operator-sdk/releases/download/${{ env.OPERATOR_SDK_VERSION }}/operator-sdk_linux_amd64 # Controller-gen make controller-gen # Kustomize make kustomize - name: Upload Support Binaries uses: actions/upload-artifact@v2 with: name: support-binaries path: ${{ steps.set-variables.outputs.bin_dir }} outputs: repository_name: ${{ steps.set-variables.outputs.repository_name }} bin_dir: ${{ steps.set-variables.outputs.bin_dir }} go_build: ${{ steps.go-cache-paths.outputs.go-build }} go_mod: ${{ steps.go-cache-paths.outputs.go-mod }} tag_event: ${{ steps.set-variables.outputs.tag_event }} dist_matrix: ${{ steps.set-variables.outputs.dist_matrix }} build-operator: runs-on: ubuntu-latest name: build-operator needs: ["setup"] strategy: matrix: platform: ${{ fromJson(needs.setup.outputs.dist_matrix) }} env: REPOSITORY_NAME: ${{ needs.setup.outputs.repository_name }} steps: - name: Set up Go 1.x uses: actions/setup-go@v1 with: go-version: ${{ inputs.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Download Support Binaries uses: actions/download-artifact@v2 with: name: support-binaries path: ${{ needs.setup.outputs.bin_dir }} - name: Prepare Build Step id: setup-build-step run: | # Setup Path echo "${{ needs.setup.outputs.bin_dir }}" >> $GITHUB_PATH # Make Binaries Executable chmod +x ${{ needs.setup.outputs.bin_dir }}/* # Configure Platform Variables echo "::set-output name=platform_os::$(echo ${{ matrix.platform }} | cut -d/ -f1)" echo "::set-output name=platform_arch::$(echo ${{ matrix.platform }} | cut -d/ -f2)" - name: Download Dependencies shell: bash run: | make generate make fmt make vet - name: build code shell: bash env: VERSION: latest GOOS: ${{ steps.setup-build-step.outputs.platform_os }} GOARCH: ${{ steps.setup-build-step.outputs.platform_arch }} run: make test-operator: runs-on: ubuntu-latest name: test-operator needs: ["setup"] env: REPOSITORY_NAME: ${{ needs.setup.outputs.repository_name }} steps: - name: Set up Go 1.x uses: actions/setup-go@v1 with: go-version: ${{ inputs.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Download Binaries uses: actions/download-artifact@v2 with: name: support-binaries path: ${{ needs.setup.outputs.bin_dir }} - name: Prepare Build Step id: setup-build-step run: | # Setup Path echo "${{ needs.setup.outputs.bin_dir }}" >> $GITHUB_PATH # Make Binaries Executable chmod +x ${{ needs.setup.outputs.bin_dir }}/* - name: Run unit tests shell: bash run: make test ================================================ FILE: .github/workflows/push.yaml ================================================ name: push on: push: branches: - main - master tags: - v* jobs: setup: runs-on: ubuntu-latest name: setup env: BUILD_PLATFORMS: "linux/amd64,linux/arm64,linux/ppc64le,linux/s390x" GO_VERSION: "~1.21" steps: - name: Setting Workflow Variables id: set-variables run: | echo "::set-output name=repository_name::$(basename $GITHUB_REPOSITORY)" echo "::set-output name=bin_dir::$(pwd)/bin" # Create Distribution Matrix echo "::set-output name=dist_matrix::$(echo -n "${{ env.BUILD_PLATFORMS }}" | jq -csR '. | split(",")')" # Set versions based on presence of tag if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then TAG="${GITHUB_REF/refs\/tags\//}" echo "::set-output name=tag_event::true" echo "::set-output name=operator_version::$TAG" else echo "::set-output name=tag_event::false" echo "::set-output name=operator_version::$DEFAULT_OPERATOR_VERSION" fi - name: Build Go Cache Paths id: go-cache-paths run: | echo "::set-output name=go-build::$(go env GOCACHE)" echo "::set-output name=go-mod::$(go env GOMODCACHE)" - name: Set up Go 1.x uses: actions/setup-go@v1 with: go-version: ${{ inputs.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v2 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Go Dependencies run: go mod download - name: Download Binaries env: OPERATOR_SDK_VERSION: ${{ inputs.OPERATOR_SDK_VERSION }} run: | # Create Binary Directory mkdir -p ${{ steps.set-variables.outputs.bin_dir }} # Operator SDK curl -L -o ${{ steps.set-variables.outputs.bin_dir }}/operator-sdk https://github.com/operator-framework/operator-sdk/releases/download/${{ env.OPERATOR_SDK_VERSION }}/operator-sdk_linux_amd64 # Controller-gen make controller-gen # Kustomize make kustomize - name: Upload Support Binaries uses: actions/upload-artifact@v2 with: name: support-binaries path: ${{ steps.set-variables.outputs.bin_dir }} outputs: repository_name: ${{ steps.set-variables.outputs.repository_name }} bin_dir: ${{ steps.set-variables.outputs.bin_dir }} go_build: ${{ steps.go-cache-paths.outputs.go-build }} go_mod: ${{ steps.go-cache-paths.outputs.go-mod }} tag_event: ${{ steps.set-variables.outputs.tag_event }} dist_matrix: ${{ steps.set-variables.outputs.dist_matrix }} build-operator: runs-on: ubuntu-latest name: build-operator needs: ["setup"] strategy: matrix: platform: ${{ fromJson(needs.setup.outputs.dist_matrix) }} env: REPOSITORY_NAME: ${{ needs.setup.outputs.repository_name }} steps: - name: Set up Go 1.x uses: actions/setup-go@v1 with: go-version: ${{ inputs.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Download Support Binaries uses: actions/download-artifact@v2 with: name: support-binaries path: ${{ needs.setup.outputs.bin_dir }} - name: Prepare Build Step id: setup-build-step run: | # Setup Path echo "${{ needs.setup.outputs.bin_dir }}" >> $GITHUB_PATH # Make Binaries Executable chmod +x ${{ needs.setup.outputs.bin_dir }}/* # Configure Platform Variables echo "::set-output name=platform_os::$(echo ${{ matrix.platform }} | cut -d/ -f1)" echo "::set-output name=platform_arch::$(echo ${{ matrix.platform }} | cut -d/ -f2)" - name: Download Dependencies shell: bash run: | make generate make fmt make vet - name: build code shell: bash env: VERSION: latest GOOS: ${{ steps.setup-build-step.outputs.platform_os }} GOARCH: ${{ steps.setup-build-step.outputs.platform_arch }} run: make test-operator: runs-on: ubuntu-latest name: test-operator needs: ["setup"] env: REPOSITORY_NAME: ${{ needs.setup.outputs.repository_name }} steps: - name: Set up Go 1.x uses: actions/setup-go@v1 with: go-version: ${{ inputs.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 - name: Go Build Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v2 with: path: ${{ needs.setup.outputs.go_mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Download Binaries uses: actions/download-artifact@v2 with: name: support-binaries path: ${{ needs.setup.outputs.bin_dir }} - name: Prepare Build Step id: setup-build-step run: | # Setup Path echo "${{ needs.setup.outputs.bin_dir }}" >> $GITHUB_PATH # Make Binaries Executable chmod +x ${{ needs.setup.outputs.bin_dir }}/* - name: Run unit tests shell: bash run: make test github-release: runs-on: ubuntu-latest name: github-release if: ${{ needs.setup.outputs.tag_event == 'true' }} needs: [ "setup", "test-operator", "build-operator", ] steps: - name: Check out code uses: actions/checkout@v2 - name: Create Release uses: softprops/action-gh-release@v1 with: generate_release_notes: true draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* # editor and IDE paraphernalia .idea *.swp *.swo *~ bundle/ bundle.Dockerfile ================================================ FILE: Dockerfile ================================================ # Build the manager binary FROM golang:1.18 as builder WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the go source COPY main.go main.go COPY api/ api/ COPY controllers/ controllers/ COPY pkg/ pkg/ # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM registry.access.redhat.com/ubi8/ubi-minimal WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT ["/manager"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # VERSION defines the project version for the bundle. # Update this value when you upgrade the version of your project. # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) VERSION ?= 0.0.1 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") # To re-generate a bundle for other specific channels without changing the standard setup, you can: # - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) # - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") ifneq ($(origin CHANNELS), undefined) BUNDLE_CHANNELS := --channels=$(CHANNELS) endif # DEFAULT_CHANNEL defines the default channel used in the bundle. # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") # To re-generate a bundle for any other default channel without changing the default setup, you can: # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") ifneq ($(origin DEFAULT_CHANNEL), undefined) BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) endif BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. # This variable is used to construct full image tags for bundle and catalog images. # # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both # example.com/memcached-operator-bundle:$VERSION and example.com/memcached-operator-catalog:$VERSION. IMAGE_TAG_BASE ?= example.com/memcached-operator # BUNDLE_IMG defines the image:tag used for the bundle. # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests # You can enable this value if you would like to use SHA Based Digests # To enable set flag to true USE_IMAGE_DIGESTS ?= false ifeq ($(USE_IMAGE_DIGESTS), true) BUNDLE_GEN_FLAGS += --use-image-digests endif # Image URL to use all building/pushing image targets IMG ?= controller:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.24.1 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif # Setting SHELL to bash allows bash commands to be executed by recipes. # This is a requirement for 'setup-envtest.sh' in the test target. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec CHART_REPO_URL ?= http://example.com HELM_REPO_DEST ?= /tmp/gh-pages .PHONY: all all: build ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk commands is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php .PHONY: help help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Development .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. go fmt ./... .PHONY: vet vet: ## Run go vet against code. go vet ./... .PHONY: test test: manifests generate fmt vet envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out ##@ Build .PHONY: build build: generate fmt vet ## Build manager binary. go build -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./main.go .PHONY: docker-build docker-build: test ## Build docker image with the manager. docker build -t ${IMG} . .PHONY: docker-push docker-push: ## Push docker image with the manager. docker push ${IMG} ##@ Deployment ifndef ignore-not-found ignore-not-found = false endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | kubectl apply -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} $(KUSTOMIZE) build config/default | kubectl apply -f - .PHONY: undeploy undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - ##@ Build Dependencies ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) ## Tool Binaries KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest ## Tool Versions KUSTOMIZE_VERSION ?= v3.8.7 CONTROLLER_TOOLS_VERSION ?= v0.9.0 KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. $(CONTROLLER_GEN): $(LOCALBIN) GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) .PHONY: envtest envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. $(ENVTEST): $(LOCALBIN) GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest .PHONY: bundle bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. operator-sdk generate kustomize manifests --interactive=false -q cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS) operator-sdk bundle validate ./bundle .PHONY: bundle-build bundle-build: ## Build the bundle image. docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . .PHONY: bundle-push bundle-push: ## Push the bundle image. $(MAKE) docker-push IMG=$(BUNDLE_IMG) .PHONY: opm OPM = ./bin/opm opm: ## Download opm locally if necessary. ifeq (,$(wildcard $(OPM))) ifeq (,$(shell which opm 2>/dev/null)) @{ \ set -e ;\ mkdir -p $(dir $(OPM)) ;\ OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ chmod +x $(OPM) ;\ } else OPM = $(shell which opm) endif endif # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). # These images MUST exist in a registry and be pull-able. BUNDLE_IMGS ?= $(BUNDLE_IMG) # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. ifneq ($(origin CATALOG_BASE_IMG), undefined) FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) endif # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator .PHONY: catalog-build catalog-build: opm ## Build a catalog image. $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) # Push the catalog image. .PHONY: catalog-push catalog-push: ## Push a catalog image. $(MAKE) docker-push IMG=$(CATALOG_IMG) # Generate helm chart .PHONY: kustomize helmchart: kustomize mkdir -p ./charts/${OPERATOR_NAME}/templates cp ./config/helmchart/templates/* ./charts/${OPERATOR_NAME}/templates $(KUSTOMIZE) build ./config/helmchart | sed 's/release-namespace/{{.Release.Namespace}}/' > ./charts/${OPERATOR_NAME}/templates/rbac.yaml version=${VERSION} envsubst < ./config/helmchart/Chart.yaml.tpl > ./charts/${OPERATOR_NAME}/Chart.yaml version=${VERSION} image_repo=$${IMG%:*} envsubst < ./config/helmchart/values.yaml.tpl > ./charts/${OPERATOR_NAME}/values.yaml helm lint ./charts/${OPERATOR_NAME} .PHONY: helmchart helmchart-repo: helmchart mkdir -p ${HELM_REPO_DEST}/${OPERATOR_NAME} helm package -d ${HELM_REPO_DEST}/${OPERATOR_NAME} ./charts/${OPERATOR_NAME} helm repo index --url ${CHART_REPO_URL} ${HELM_REPO_DEST} .PHONY: helmchart-repo helmchart-repo-push: helmchart-repo git -C ${HELM_REPO_DEST} add . git -C ${HELM_REPO_DEST} status git -C ${HELM_REPO_DEST} commit -m "Release ${VERSION}" git -C ${HELM_REPO_DEST} push origin "gh-pages" ================================================ FILE: PROJECT ================================================ domain: example.io layout: - go.kubebuilder.io/v3 projectName: operator-utils repo: github.com/redhat-cop/operator-utils resources: - domain: example.io group: operator-utils kind: MyCRD path: github.com/redhat-cop/operator-utils/api/v1alpha1 version: v1alpha1 - domain: example.io group: operator-utils kind: EnforcingCRD path: github.com/redhat-cop/operator-utils/api/v1alpha1 version: v1alpha1 - domain: example.io group: operator-utils kind: EnforcingPatch path: github.com/redhat-cop/operator-utils/api/v1alpha1 version: v1alpha1 - domain: example.io group: operator-utils kind: EnforcingPatch path: github.com/redhat-cop/operator-utils/api/v1alpha1 version: v1alpha1 version: "3" plugins: manifests.sdk.operatorframework.io/v2: {} scorecard.sdk.operatorframework.io/v2: {} ================================================ FILE: README.md ================================================ # Operator Utility Library ![build status](https://github.com/redhat-cop/operator-utils/workflows/push/badge.svg) [![GoDoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/redhat-cop/operator-utils) [![Go Report Card](https://goreportcard.com/badge/github.com/redhat-cop/operator-utils)](https://goreportcard.com/report/github.com/redhat-cop/operator-utils) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/redhat-cop/operator-utils) This library layers on top of the Operator SDK and with the objective of helping writing better and more consistent operators. *NOTICE* versions of this library up to `v0.3.7` are compatible with [operator-sdk](https://github.com/operator-framework/operator-sdk) `0.x`, starting from version v0.4.0 this library will be compatible only with [operator-sdk](https://github.com/operator-framework/operator-sdk) 1.x. ## Scope of this library This library covers three main areas: 1. [Utility Methods](#Utility-Methods) Utility methods that are callable by any operator. 2. [Idempotent methods](#Idempotent-Methods-to-Manipulate-Resources) to manipulate resources and arrays of resources 3. [Basic operator lifecycle](#Basic-Operator-Lifecycle-Management) needs (validation, initialization, status and error management, finalization) 4. [Enforcing resources operator support](#Enforcing-Resource-Operator-Support). For those operators which calculate a set of resources that need to exist and then enforce them, generalized support for the enforcing phase is provided. ## Utility Methods Prior to version v1.3.x the general philosophy of this library was that new operator would inherit from `ReconcilerBase` and in doing so they would have access to a bunch of utility methods. With release v1.3.0 a new approach is available. Utility methods are callable by any operator without having to inherit. This makes it easier to use this library and does not conflict with autogenerate code from `kube-builder` and `operator-sdk`. Most of the Utility methods receive a context.Context parameter. Normally this context must be initialized with a `logr.Logger` and a `rest.Config`. Some utility methods may require more, see each individual documentation. Utility methods are currently organized in the following folders: 1. crud: idempotent create/update/delete functions. 2. discoveryclient: methods related to the discovery client, typically used to load `apiResource` objects. 3. dynamicclient: methods related to building client based on object whose type is not known at compile time. 4. templates: utility methods for dealing with templates whose output is an object or a list of objects. ## Idempotent Methods to Manipulate Resources The following idempotent methods are provided (and their corresponding array version): 1. createIfNotExists 2. createOrUpdate 3. deleteIfExists Also there are utility methods to manage finalizers, test ownership and process templates of resources. ## Basic Operator Lifecycle Management --- Note This part of the library is largely deprecated. For initialization and defaulting a MutatingWebHook should be used. For validation a Validating WebHook should be used. The part regarding the finalization is still relevant. --- To get started with this library do the following: Change your reconciler initialization as exemplified below to add a set of utility methods to it ```go import "github.com/redhat-cop/operator-utils/pkg/util" ... type MyReconciler struct { util.ReconcilerBase Log logr.Logger ... other optional fields ... } ``` in main.go change like this ```go if err = (&controllers.MyReconciler{ ReconcilerBase: util.NewReconcilerBase(mgr.GetClient(), mgr.GetScheme(), mgr.GetConfig(), mgr.GetEventRecorderFor("My_controller"), mgr.GetAPIReader()), Log: ctrl.Log.WithName("controllers").WithName("My"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "My") os.Exit(1) } ``` Also make sure to create the manager with `configmap` as the lease option for leader election: ```go mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, Port: 9443, LeaderElection: enableLeaderElection, LeaderElectionID: "dcb036b8.redhat.io", LeaderElectionResourceLock: "configmaps", }) ``` If you want status management, add this to your CRD: ```go // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } func (m *MyCRD) GetConditions() []metav1.Condition { return m.Status.Conditions } func (m *MyCRD) SetConditions(conditions []metav1.Condition) { m.Status.Conditions = conditions } ``` At this point your controller is able to leverage the utility methods of this library: 1. [managing CR validation](#managing-cr-validation) 2. [managing CR initialization](#managing-cr-initialization) 3. [managing status and error conditions](#managing-status-and-error-conditions) 4. [managing CR finalization](#managing-cr-finalization) 5. high-level object manipulation functions such as: - createOrUpdate, createIfNotExists, deleteIfExists - same functions on an array of objects - go template processing of objects A full example is provided [here](./controllers/mycrd_controller.go) ### Managing CR validation To enable CR validation add this to your controller: ```go if ok, err := r.IsValid(instance); !ok { return r.ManageError(ctx, instance, err) } ``` The implement the following function: ```go func (r *ReconcileMyCRD) IsValid(obj metav1.Object) (bool, error) { mycrd, ok := obj.(*examplev1alpha1.MyCRD) ... } ``` ### Managing CR Initialization To enable CR initialization, add this to your controller: ```go if ok := r.IsInitialized(instance); !ok { err := r.GetClient().Update(context.TODO(), instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(ctx, instance, err) } return reconcile.Result{}, nil } ``` Then implement the following function: ```go func (r *ReconcileMyCRD) IsInitialized(obj metav1.Object) bool { mycrd, ok := obj.(*examplev1alpha1.MyCRD) } ``` ### Managing Status and Error Conditions To update the status with success and return from the reconciliation cycle, code the following: ```go return r.ManageSuccess(ctx, instance) ``` To update the status with failure, record an event and return from the reconciliation cycle, code the following: ```go return r.ManageError(ctx, instance, err) ``` notice that this function will reschedule a reconciliation cycle with increasingly longer wait time up to six hours. There are also variants of these calls to allow for requeuing after a given delay. Requeuing is handy when reconciliation depends on a cluster-external state which is not observable from within the api-server. ```go return r.ManageErrorWithRequeue(ctx, instance, err, 3*time.Second) ``` ```go return r.ManageSuccessWithRequeue(ctx, instance, 3*time.Second) ``` or simply using the convenience function: ```go return r.ManageOutcomeWithRequeue(ctx, instance, err, 3*time.Second) ``` which will delegate to the error or success variant depending on `err` being `nil` or not. ### Managing CR Finalization to enable CR finalization add this to your controller: ```go if util.IsBeingDeleted(instance) { if !util.HasFinalizer(instance, controllerName) { return reconcile.Result{}, nil } err := r.manageCleanUpLogic(instance) if err != nil { log.Error(err, "unable to delete instance", "instance", instance) return r.ManageError(ctx, instance, err) } util.RemoveFinalizer(instance, controllerName) err = r.GetClient().Update(context.TODO(), instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(ctx, instance, err) } return reconcile.Result{}, nil } ``` Then implement this method: ```go func (r *ReconcileMyCRD) manageCleanUpLogic(mycrd *examplev1alpha1.MyCRD) error { ... } ``` ## Support for operators that need to enforce a set of resources to a defined state Many operators have the following logic: 1. Phase 1: based on the CR and potentially additional status, a set of resources that need to exist is calculated. 2. Phase 2: These resources are then created or updated against the master API. 3. Phase 3: A well written operator also ensures that these resources stay in place and are not accidentally or maliciously changed by third parties. These phases are of increasing difficulty to implement. It's also true that phase 2 and 3 can be generalized. Operator-utils offers some scaffolding to assist in writing these kinds of operators. Similarly to the `BaseReconciler` class, we have a base type to extend called: `EnforcingReconciler`. This class extends from `BaseReconciler`, so you have all the same facilities as above. When initializing the EnforcingReconciler, one must chose whether watchers will be created at the cluster level or at the namespace level. - if cluster level is chosen a watch per CR and type defined in it will be created. This will require the operator to have cluster level access. - if namespace level watchers is chosen a watch per CR, type and namespace will be created. This will minimize the needed permissions, but depending on what the operator needs to do may open a very high number of connections to the API server. The body of the reconciler function will look something like this: ```golang validation... initialization... (optional) finalization... Phase1 ... calculate a set of resources to be enforced -> LockedResources err = r.UpdateLockedResources(context,instance, lockedResources, ...) if err != nil { log.Error(err, "unable to update locked resources") return r.ManageError(ctx, instance, err) } return r.ManageSuccess(ctx, instance) ``` this is all you have to do for basic functionality. For more details see the [example](pkg/controller/apis/enforcingcrd/enforcingcrd_controller.go) the EnforcingReconciler will do the following: 1. restore the resources to the desired stated if the are changed. Notice that you can exclude paths from being considered when deciding whether to restore a resource. As set of JSON Path can be passed together with the LockedResource. It is recommended to set these paths: 1. `.metadata` 2. `.status` 2. restore resources when they are deleted. The `UpdateLockedResources` will validate the input as follows: 1. the passed resource must be defined in the current apiserver 2. the passed resource must be syntactically compliant with the OpenAPI definition of the resource defined in the server. 3. if the passed resource is namespaced, the namespace field must be initialized. The finalization method will look like this: ```golang func (r *ReconcileEnforcingCRD) manageCleanUpLogic(instance *examplev1alpha1.EnforcingCRD) error { err := r.Terminate(instance, true) if err != nil { log.Error(err, "unable to terminate enforcing reconciler for", "instance", instance) return err } ... additional finalization logic ... return nil } ``` Convenience methods are also available for when resources are templated. See the [templatedenforcingcrd](./pkgcontroller/templatedenforcingcrd/templatedenforcingcrd_controller.go) controller as an example. ## Support for operators that need to enforce a set of patches For similar reasons stated in the previous paragraphs, operators might need to enforce patches. A patch modifies an object created by another entity. Because in this case the CR does not own the to-be-modified object a patch must be enforced against changes made on it. One must be careful not to create circular situations where an operator deletes the patch and this operator recreates the patch. In some situations, a patch must be parametric on some state of the cluster. For this reason, it's possible to monitor source objects that will be used as parameters to calculate the patch. A patch is defined as follows: ```golang type LockedPatch struct { Name string `json:"name,omitempty"` SourceObjectRefs []utilsapi.SourceObjectReference `json:"sourceObjectRefs,omitempty"` TargetObjectRef utilsapi.TargetObjectReference `json:"targetObjectRef,omitempty"` PatchType types.PatchType `json:"patchType,omitempty"` PatchTemplate string `json:"patchTemplate,omitempty"` Template template.Template `json:"-"` } ``` the targetObjectRef and sourceObjectRefs are watched for changes by the reconciler. targetObjectRef can select multiple objects, this is the logic | Namespaced Type | Namespace | Name | Selection type | | --- | --- | --- | --- | | yes | null | null | multiple selection across namespaces | | yes | null | not null | multiple selection across namespaces where the name corresponds to the passed name | | yes | not null | null | multiple selection within a namespace | | yes | not null | not nul | single selection | | no | N/A | null | multiple selection | | no | N/A | not null | single selection | Selection can be further narrowed down by filtering by labels and/or annotations. The patch will be applied to all of the selected instances. Name and Namespace of sourceRefObjects are interpreted as golang templates with the current target instance and the only parameter. This allows to select different source object for each target object. The relevant part of the operator code would look like this: ```golang validation... initialization... Phase1 ... calculate a set of patches to be enforced -> LockedPatches err = r.UpdateLockedResources(context, instance, ..., lockedPatches...) if err != nil { log.Error(err, "unable to update locked resources") return r.ManageError(ctx, instance, err) } return r.ManageSuccess(ctx, instance) ``` The `UpdateLockedResources` will validate the input as follows: 1. the passed patch target/source `ObjectRef` resource must be defined in the current apiserver 2. if the passed patch target/source `ObjectRef` resources are namespaced the corresponding namespace field must be initialized. 3. the ID must have a not null and unique value in the array of the passed patches. Patches cannot be undone so there is no need to manage a finalizer. [Here](./pkg/controller/enforcingpatch/enforcingpatch_controller.go) you can find an example of how to implement an operator with this the ability to enforce patches. ## Support for operators that need dynamic creation of locked resources using templates Operators may also need to leverage locked resources created dynamically through templates. This can be done using [go templates](https://golang.org/pkg/text/template/) and leveraging the `GetLockedResourcesFromTemplates` function. ```golang lockedResources, err := r.GetLockedResourcesFromTemplates(templates..., params...) if err != nil { log.Error(err, "unable to process templates with param") return err } ``` The `GetLockedResourcesFromTemplates` will validate the input as follows: 1. check that the passed template is valid 2. format the template using the properties of the passed object in the params parameter 3. create an array of `LockedResource` objects based on parsed template The example below shows how templating can be used to reference the name of the resource passed as the parameter and use it as a property in the creation of the `LockedResource`. ```golang objectTemplate: | apiVersion: v1 kind: Namespace metadata: name: {{ .Name }} ``` This functionality can leverage advanced features of go templating, such as loops, to generate more than one object following a set pattern. The below example will create an array of namespace `LockedResources` using the title of any key where the associated value matches the text *devteam* in the key/value pair of the `Labels` property of the resource passed in the params parameter. ```golang objectTemplate: | {{range $key, $value := $.Labels}} {{if eq $value "devteam"}} - apiVersion: v1 kind: Namespace metadata: name: {{ $key }} {{end}} {{end}} ``` ## Support for operators that need advanced templating functionality Operators may need to utilize advanced templating functions not found in the base go templating library. This advanced template functionality matches the same available in the popular k8s management tool [Helm](https://helm.sh/). `LockedPatch` templates uses this functionality by default. To utilize these features when using `LockedResources` the following function is required, ```golang lockedResources, err := r.GetLockedResourcesFromTemplatesWithRestConfig(templates..., rest.Config..., params...) if err != nil { log.Error(err, "unable to process templates with param") return err } ``` ## Deployment ### Deploying with Helm Here are the instructions to install the latest release with Helm. ```shell oc new-project operator-utils helm repo add operator-utils https://redhat-cop.github.io/operator-utils helm repo update helm install operator-utils operator-utils/operator-utils ``` This can later be updated with the following commands: ```shell helm repo update helm upgrade operator-utils operator-utils/operator-utils ``` ## Development ## Running the operator locally ```shell make install oc new-project operator-utils-operator-local kustomize build ./config/local-development | oc apply -f - -n operator-utils-operator-local export token=$(oc serviceaccounts get-token 'operator-utils-operator-controller-manager' -n operator-utils-operator-local) oc login --token ${token} make run ENABLE_WEBHOOKS=false ``` ### testing Patches ```shell oc new-project patch-test oc create sa test -n patch-test oc adm policy add-cluster-role-to-user cluster-admin -z default -n patch-test oc apply -f ./test/enforcing-patch.yaml -n patch-test oc apply -f ./test/enforcing-patch-multiple.yaml -n patch-test oc apply -f ./test/enforcing-patch-multiple-cluster-level.yaml -n patch-test ``` ## Building/Pushing the operator image ```shell export repo=raffaelespazzoli #replace with yours docker login quay.io/$repo make docker-build IMG=quay.io/$repo/operator-utils:latest make docker-push IMG=quay.io/$repo/operator-utils:latest ``` ## Deploy to OLM via bundle ```shell make manifests make bundle IMG=quay.io/$repo/operator-utils:latest operator-sdk bundle validate ./bundle --select-optional name=operatorhub make bundle-build BUNDLE_IMG=quay.io/$repo/operator-utils-bundle:latest docker push quay.io/$repo/operator-utils-bundle:latest operator-sdk bundle validate quay.io/$repo/operator-utils-bundle:latest --select-optional name=operatorhub oc new-project operator-utils oc label namespace operator-utils openshift.io/cluster-monitoring="true" operator-sdk cleanup operator-utils -n operator-utils operator-sdk run bundle --install-mode AllNamespaces -n operator-utils quay.io/$repo/operator-utils-bundle:latest ``` ## Releasing ```shell git tag -a "" -m "" git push upstream ``` If you need to remove a release: ```shell git tag -d git push upstream --delete ``` If you need to "move" a release to the current main ```shell git tag -f git push upstream -f ``` ### Cleaning up ```shell operator-sdk cleanup operator-utils -n operator-utils oc delete operatorgroup operator-sdk-og oc delete catalogsource operator-utils-catalog ``` ================================================ FILE: api/v1alpha1/enforcingcrd_types.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // EnforcingCRDSpec defines the desired state of EnforcingCRD type EnforcingCRDSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // Resources is a list of resource manifests that should be locked into the specified configuration // +kubebuilder:validation:Optional // +listType=atomic Resources []LockedResource `json:"resources,omitempty"` } // EnforcingCRDStatus defines the observed state of EnforcingCRD type EnforcingCRDStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // +kubebuilder:validation:Optional EnforcingReconcileStatus `json:",inline,omitempty"` } func (m *EnforcingCRD) GetEnforcingReconcileStatus() EnforcingReconcileStatus { return m.Status.EnforcingReconcileStatus } func (m *EnforcingCRD) SetEnforcingReconcileStatus(reconcileStatus EnforcingReconcileStatus) { m.Status.EnforcingReconcileStatus = reconcileStatus } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // EnforcingCRD is the Schema for the enforcingcrds API type EnforcingCRD struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec EnforcingCRDSpec `json:"spec,omitempty"` Status EnforcingCRDStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // EnforcingCRDList contains a list of EnforcingCRD type EnforcingCRDList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []EnforcingCRD `json:"items"` } func init() { SchemeBuilder.Register(&EnforcingCRD{}, &EnforcingCRDList{}) } ================================================ FILE: api/v1alpha1/enforcingpatch_types.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // EnforcingPatchSpec defines the desired state of EnforcingPatch type EnforcingPatchSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // Patches is a list of pacthes that should be encforced at runtime. // +kubebuilder:validation:Optional Patches map[string]PatchSpec `json:"patches,omitempty"` } // EnforcingPatchStatus defines the observed state of EnforcingPatch type EnforcingPatchStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html EnforcingReconcileStatus `json:",inline,omitempty"` } func (m *EnforcingPatch) GetEnforcingReconcileStatus() EnforcingReconcileStatus { return m.Status.EnforcingReconcileStatus } func (m *EnforcingPatch) SetEnforcingReconcileStatus(reconcileStatus EnforcingReconcileStatus) { m.Status.EnforcingReconcileStatus = reconcileStatus } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // EnforcingPatch is the Schema for the enforcingpatches API type EnforcingPatch struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec EnforcingPatchSpec `json:"spec,omitempty"` Status EnforcingPatchStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // EnforcingPatchList contains a list of EnforcingPatch type EnforcingPatchList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []EnforcingPatch `json:"items"` } func init() { SchemeBuilder.Register(&EnforcingPatch{}, &EnforcingPatchList{}) } ================================================ FILE: api/v1alpha1/enforcingreconcilerstatus.go ================================================ package v1alpha1 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +listType=map // +listMapKey=type type Conditions []metav1.Condition // +mapType=granular type ConditionMap map[string]Conditions // EnforcingReconcileStatus represents the status of the last reconcile cycle. It's used to communicate success or failure and the error message type EnforcingReconcileStatus struct { // ReconcileStatus this is the general status of the main reconciler // +kubebuilder:validation:Optional // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` //LockedResourceStatuses contains the reconcile status for each of the managed resources // +kubebuilder:validation:Optional LockedResourceStatuses map[string]Conditions `json:"lockedResourceStatuses,omitempty"` //LockedResourceStatuses contains the reconcile status for each of the managed resources // +kubebuilder:validation:Optional LockedPatchStatuses map[string]ConditionMap `json:"lockedPatchStatuses,omitempty"` } // EnforcingReconcileStatusAware is an interfce that must be implemented by a CRD type that has been enabled with ReconcileStatus, it can then benefit of a series of utility methods. // +kubebuilder:object:generate:=false type EnforcingReconcileStatusAware interface { GetEnforcingReconcileStatus() EnforcingReconcileStatus SetEnforcingReconcileStatus(enforcingReconcileStatus EnforcingReconcileStatus) } ================================================ FILE: api/v1alpha1/groupversion_info.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package v1alpha1 contains API Schema definitions for the operator-utils v1alpha1 API group // +kubebuilder:object:generate=true // +groupName=operator-utils.example.io package v1alpha1 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) var ( // GroupVersion is group version used to register these objects GroupVersion = schema.GroupVersion{Group: "operator-utils.example.io", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) ================================================ FILE: api/v1alpha1/lockedpatch.go ================================================ package v1alpha1 import ( "bytes" "context" "errors" "text/template" "github.com/redhat-cop/operator-utils/pkg/util/discoveryclient" "github.com/redhat-cop/operator-utils/pkg/util/dynamicclient" utiltemplates "github.com/redhat-cop/operator-utils/pkg/util/templates" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) // Patch describes a patch to be enforced at runtime // +k8s:openapi-gen=true type PatchSpec struct { //Name represents a unique name for this patch, it has no particular effect, except for internal bookeeping // SourceObjectRefs is an arrays of refereces to source objects that will be used as input for the template processing. These refernces must resolve to single instance. The resolution rule is as follows (+ present, - absent): // the King and APIVersion field are mandatory // +Namespace +Name: resolves to object / // +Namespace -Name: results in an error // -Namespace +Name: resolves to cluster-level object . If Kind is namespaced, this results in an error. // -Namespace -Name: results in an error // Name manespaces Namespace are evaluated as golang templates with the input of the template being the target object. When selecting multiple target, this allows for having specific source objects for each target. // ResourceVersion and UID are always ignored // If FieldPath is specified, the restuned object is calculated from the path, so for example if FieldPath=.spec, the only the spec portion of the object is returned. // The target object is always added as element zero of the array of the SourceObjectRefs // +kubebuilder:validation:Optional // +listType=atomic SourceObjectRefs []SourceObjectReference `json:"sourceObjectRefs,omitempty"` // TargetObjectRef is a reference to the object to which the pacth should be applied. // the King and APIVersion field are mandatory // the Name and Namespace field have the following meaning (+ present, - absent) // +Namespace +Name: apply the patch to the object: / // +Namespace -Name: apply the patch to all of the objects in // -Namespace +Name: apply the patch to the cluster-level object . If Kind is namespaced, this results in an error. // -Namespace -Name: if the kind is namespaced apply the patch to all of the objects in all of the namespaces. If the kind is not namespaced, apply the patch to all of the cluster level objects. // The lable selector can be used to further filter the selected objects. // +kubebuilder:validation:Required TargetObjectRef TargetObjectReference `json:"targetObjectRef,omitempty"` // PatchType is the type of patch to be applied, one of "application/json-patch+json"'"application/merge-patch+json","application/strategic-merge-patch+json","application/apply-patch+yaml" // +kubebuilder:validation:Required // +kubebuilder:validation:Enum="application/json-patch+json";"application/merge-patch+json";"application/strategic-merge-patch+json";"application/apply-patch+yaml" // default:="application/strategic-merge-patch+json" PatchType types.PatchType `json:"patchType,omitempty"` // PatchTemplate is a go template that will be resolved using the SourceObjectRefs as parameters. The result must be a valid patch based on the pacth type and the target object. // +kubebuilder:validation:Required PatchTemplate string `json:"patchTemplate,omitempty"` } type TargetObjectReference struct { // API version of the referent. // +kubebuilder:validation:Required APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,5,opt,name=apiVersion"` // Kind of the referent. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds // +kubebuilder:validation:Required Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"` // Namespace of the referent. // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ // +kubebuilder:validation:Optional Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` // Name of the referent. // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names // +kubebuilder:validation:Optional Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` // LabelSelector selects objects by label // +kubebuilder:validation:Optional LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` // AnnotationSelector selects objects by label AnnotationSelector *metav1.LabelSelector `json:"annotationSelector,omitempty"` //apiResource caches apiResource for this targetReference apiResource *metav1.APIResource `json:"-"` } func (t *TargetObjectReference) getAPIReourceForGVK(context context.Context) (*metav1.APIResource, bool, error) { if t.apiResource != nil { return t.apiResource, true, nil } apiresource, found, err := discoveryclient.GetAPIResourceForGVK(context, schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)) if err != nil && found { t.apiResource = apiresource } return apiresource, found, err } func (t *TargetObjectReference) getDynamicClient(context context.Context) (dynamic.ResourceInterface, error) { log := log.FromContext(context) _, namespacedSelection, err := t.IsSelectingMultipleInstances(context) if err != nil { log.Error(err, "unable to determine if the target reference is selecting multiple instance", "targetReference", t) return nil, err } var ri dynamic.ResourceInterface nri, namespaced, err := dynamicclient.GetDynamicClientForGVK(context, schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)) if err != nil { log.Error(err, "unable to get dynamicClient on ", "gvk", schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)) return nil, err } if namespaced && namespacedSelection { ri = nri.Namespace(t.Namespace) } else { ri = nri } return ri, nil } func (t *TargetObjectReference) GetReferencedObjectWithName(context context.Context, namespacedName types.NamespacedName) (*unstructured.Unstructured, error) { log := log.FromContext(context) multiple, _, err := t.IsSelectingMultipleInstances(context) if err != nil { log.Error(err, "unable to determine if the target reference is selecting multiple instance", "targetReference", t) return nil, err } if !multiple { return t.GetReferencedObject(context) } targetCopy := t.DeepCopy() targetCopy.Name = namespacedName.Name namespaced, err := t.IsNamespaced(context) if err != nil { log.Error(err, "unable to determine if the target reference is namespaced", "targetReference", t) return nil, err } if namespaced { targetCopy.Namespace = namespacedName.Namespace } client, err := targetCopy.getDynamicClient(context) if err != nil { log.Error(err, "unable to get dynamic client with", "targetReference", targetCopy) return nil, err } obj, err := client.Get(context, targetCopy.Name, metav1.GetOptions{}) if err != nil { log.Error(err, "unable to get referenced object", "targetReference", targetCopy) return nil, err } return obj, nil } func (t *TargetObjectReference) GetReferencedObject(context context.Context) (*unstructured.Unstructured, error) { log := log.FromContext(context) multiple, _, err := t.IsSelectingMultipleInstances(context) if err != nil { log.Error(err, "unable to determine if the target reference is selecting multiple instance", "targetReference", t) return nil, err } if multiple { return nil, errors.New("cannot call this method on a target that selects multiple instances") } dclient, err := t.getDynamicClient(context) if err != nil { log.Error(err, "unable to get dynamic client on", "targetReference", t) return nil, err } obj, err := dclient.Get(context, t.Name, metav1.GetOptions{}) if err != nil { log.Error(err, "unable to get referenced ", "object", t) return nil, err } return obj, nil } func (t *TargetObjectReference) GetReferencedObjects(context context.Context) ([]unstructured.Unstructured, error) { log := log.FromContext(context) multiple, _, err := t.IsSelectingMultipleInstances(context) if err != nil { log.Error(err, "unable to determine if the target reference is selecting multiple instance", "targetReference", t) return nil, err } if !multiple { return nil, errors.New("cannot call this method on a target that does not select multiple instances") } dclient, err := t.getDynamicClient(context) if err != nil { log.Error(err, "unable to get dynamic client on", "targetReference", t) return nil, err } labelSelector, err := metav1.LabelSelectorAsSelector(t.LabelSelector) if err != nil { log.Error(err, "unable to process ", "labelSelector", t.LabelSelector) return nil, err } objList, err := dclient.List(context, metav1.ListOptions{ LabelSelector: labelSelector.String(), }) if err != nil { log.Error(err, "unable to list referenced ", "objects", t) return nil, err } var annotatonSelector labels.Selector if t.AnnotationSelector != nil { annotatonSelector, err = metav1.LabelSelectorAsSelector(t.AnnotationSelector) if err != nil { return nil, err } } else { annotatonSelector = labels.Everything() } //filter by annotation annotationFilteredList := []unstructured.Unstructured{} for i := range objList.Items { if annotatonSelector.Matches(labels.Set(objList.Items[i].GetAnnotations())) { annotationFilteredList = append(annotationFilteredList, objList.Items[i]) } } //filter by name if t.Name != "" { filteredList := []unstructured.Unstructured{} for i := range annotationFilteredList { if t.Name == annotationFilteredList[i].GetName() { filteredList = append(filteredList, annotationFilteredList[i]) } } return filteredList, nil } return objList.Items, nil } func (t *TargetObjectReference) IsNamespaced(context context.Context) (bool, error) { apiresource, found, err := t.getAPIReourceForGVK(context) if err != nil { return false, err } if !found { return false, errors.New("resource not found" + schema.FromAPIVersionAndKind(t.APIVersion, t.Kind).String()) } return apiresource.Namespaced, nil } // IsSelectingMultipleInstances is a helper function to determine whether this targetObjectReference selects one or multiple instance. func (t *TargetObjectReference) IsSelectingMultipleInstances(context context.Context) (multiple bool, namespacedSelection bool, err error) { log := log.FromContext(context) namespaced, err := t.IsNamespaced(context) if err != nil { log.Error(err, "Unable to determine if targetObjectReference is namespaced", "TargetObjectReference", t) return false, false, err } if namespaced { if t.Namespace == "" { return true, false, nil } else { if t.Name == "" { return true, true, nil } else { return false, true, nil } } } else { return t.Name == "", false, nil } } // Selects returns whether the passed object is selected by the current target reference // requires context with log and restConfig func (t *TargetObjectReference) Selects(context context.Context, obj client.Object) (bool, error) { log := log.FromContext(context) if apiversion, kind := obj.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind(); t.Kind != kind || t.APIVersion != apiversion { return false, nil } var labelSelector labels.Selector var annotatonSelector labels.Selector var err error if t.LabelSelector != nil { labelSelector, err = metav1.LabelSelectorAsSelector(t.LabelSelector) if err != nil { return false, err } } else { labelSelector = labels.Everything() } if t.AnnotationSelector != nil { annotatonSelector, err = metav1.LabelSelectorAsSelector(t.AnnotationSelector) if err != nil { return false, err } } else { annotatonSelector = labels.Everything() } namespaced, err := discoveryclient.IsGVKNamespaced(context, obj.GetObjectKind().GroupVersionKind()) if err != nil { log.Error(err, "Unable to determine if GVK is namespaced", "GVK", obj.GetObjectKind().GroupVersionKind()) return false, err } if namespaced { if t.Namespace != "" { //we are selecting within a namespace if t.Namespace != obj.GetNamespace() { return false, nil } } if t.Name != "" { // we are matching on name return t.Name == obj.GetName(), nil } else { // we select via selectors return labelSelector.Matches(labels.Set(obj.GetLabels())) && annotatonSelector.Matches(labels.Set(obj.GetAnnotations())), nil } } else { //cluster object, we ignore namespace if t.Name != "" { // we select via name return t.Name == obj.GetName(), nil } else { // we select via selectors return labelSelector.Matches(labels.Set(obj.GetLabels())) && annotatonSelector.Matches(labels.Set(obj.GetAnnotations())), nil } } } // GetNameAndNamespace processes the templates for Name and Namespace of the sourceObjectReference // requires context with log and restConfig func (s *SourceObjectReference) GetNameAndNamespace(context context.Context, target *unstructured.Unstructured) (name string, namespace string, err error) { log := log.FromContext(context) name, err = processTemplate(context, s.Name, target.UnstructuredContent()) if err != nil { log.Error(err, "unable to process template for", "name", s.Name) return "", "", err } namespace, err = processTemplate(context, s.Namespace, target.UnstructuredContent()) if err != nil { log.Error(err, "unable to process template for", "namespace", s.Name) return "", "", err } return } func (t *SourceObjectReference) getAPIReourceForGVK(context context.Context) (*metav1.APIResource, bool, error) { if t.apiResource != nil { return t.apiResource, true, nil } apiresource, found, err := discoveryclient.GetAPIResourceForGVK(context, schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)) if err != nil && found { t.apiResource = apiresource } return apiresource, found, err } func (t *SourceObjectReference) getDynamicClient(context context.Context) (dynamic.ResourceInterface, error) { log := log.FromContext(context) var ri dynamic.ResourceInterface nri, namespaced, err := dynamicclient.GetDynamicClientForGVK(context, schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)) if err != nil { log.Error(err, "unable to get dynamicClient on ", "gvk", schema.FromAPIVersionAndKind(t.APIVersion, t.Kind)) return nil, err } if namespaced { ri = nri.Namespace(t.Namespace) } else { ri = nri } return ri, nil } func (s *SourceObjectReference) GetReferencedObject(context context.Context, target *unstructured.Unstructured) (*unstructured.Unstructured, error) { log := log.FromContext(context) name, namespace, err := s.GetNameAndNamespace(context, target) if err != nil { log.Error(err, "unable to get name and namespaces on ", "SourceObjectReference", s, "with target", target) return nil, err } sourceCopy := s.DeepCopy() sourceCopy.Name = name sourceCopy.Namespace = namespace client, err := sourceCopy.getDynamicClient(context) if err != nil { log.Error(err, "unable to get dynamic client for ", "source", sourceCopy) return nil, err } obj, err := client.Get(context, name, metav1.GetOptions{}) if err != nil { log.Error(err, "unable to get referenced object ", "sourceCopy", sourceCopy) return nil, err } return obj, nil } func processTemplate(context context.Context, templateString string, param interface{}) (string, error) { log := log.FromContext(context) restConfig := context.Value("restConfig").(*rest.Config) template, err := template.New(templateString).Funcs(utiltemplates.AdvancedTemplateFuncMap(restConfig, log)).Parse(templateString) if err != nil { log.Error(err, "unable to parse", "template", templateString) return "", err } var b bytes.Buffer err = template.Execute(&b, param) if err != nil { log.Error(err, "unable to process", "template", templateString, "with param", param) return "", err } return b.String(), nil } type SourceObjectReference struct { // API version of the referent. // +kubebuilder:validation:Required APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,5,opt,name=apiVersion"` // Kind of the referent. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds // +kubebuilder:validation:Required Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"` // Namespace of the referent. // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ // +kubebuilder:validation:Optional Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"` // Name of the referent. // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names // +kubebuilder:validation:Optional Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` // If referring to a piece of an object instead of an entire object, this string // should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. // For example, if the object reference is to a container within a pod, this would take on a value like: // "spec.containers{name}" (where "name" refers to the name of the container that triggered // the event) or if no container name is specified "spec.containers[2]" (container with // index 2 in this pod). This syntax is chosen only to have some well-defined way of // referencing a part of an object. // +kubebuilder:validation:Optional FieldPath string `json:"fieldPath,omitempty" protobuf:"bytes,7,opt,name=fieldPath"` //apiResource caches apiResource for this targetReference apiResource *metav1.APIResource `json:"-"` } ================================================ FILE: api/v1alpha1/lockedresource.go ================================================ package v1alpha1 import ( "k8s.io/apimachinery/pkg/runtime" ) // LockedResource represents a resource to be enforced in a LockedResourceController and can be used in a API specification // +k8s:openapi-gen=true type LockedResource struct { // Object is a yaml representation of an API resource // +kubebuilder:validation:Required Object runtime.RawExtension `json:"object"` // ExludedPaths are a set of json paths that need not be considered by the LockedResourceReconciler // +kubebuilder:validation:Optional // +listType=set ExcludedPaths []string `json:"excludedPaths,omitempty"` } // LockedResourceTemplate represents a resource template in go language to be enforced in a LockedResourceController and can be used in a API specification // +k8s:openapi-gen=true type LockedResourceTemplate struct { // ObjectTemplate is a goland template. Whne processed, it must resolve to a yaml representation of an API resource // +kubebuilder:validation:Required ObjectTemplate string `json:"objectTemplate"` // ExludedPaths are a set of json paths that need not be considered by the LockedResourceReconciler // +kubebuilder:validation:Optional // +listType=set ExcludedPaths []string `json:"excludedPaths,omitempty"` } ================================================ FILE: api/v1alpha1/mycrd_types.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // MyCRDSpec defines the desired state of MyCRD type MyCRDSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html Initialized bool `json:"initialized"` Valid bool `json:"valid"` Error bool `json:"error"` } // MyCRDStatus defines the observed state of MyCRD type MyCRDStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html // // +patchMergeKey=type // // +patchStrategy=merge // // +listType=map // // +listMapKey=type // Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } func (m *MyCRD) GetConditions() []metav1.Condition { return m.Status.Conditions } func (m *MyCRD) SetConditions(conditions []metav1.Condition) { m.Status.Conditions = conditions } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // MyCRD is the Schema for the mycrds API type MyCRD struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MyCRDSpec `json:"spec,omitempty"` Status MyCRDStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // MyCRDList contains a list of MyCRD type MyCRDList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []MyCRD `json:"items"` } func init() { SchemeBuilder.Register(&MyCRD{}, &MyCRDList{}) } ================================================ FILE: api/v1alpha1/templatedenforcingcrd_types.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // TemplatedEnforcingCRDSpec defines the desired state of TemplatedEnforcingCRD type TemplatedEnforcingCRDSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // +kubebuilder:validation:Optional // +listType=atomic Templates []LockedResourceTemplate `json:"templates,omitempty"` } // TemplatedEnforcingCRDStatus defines the observed state of TemplatedEnforcingCRD type TemplatedEnforcingCRDStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // +kubebuilder:validation:Optional EnforcingReconcileStatus `json:",inline,omitempty"` } func (m *TemplatedEnforcingCRD) GetEnforcingReconcileStatus() EnforcingReconcileStatus { return m.Status.EnforcingReconcileStatus } func (m *TemplatedEnforcingCRD) SetEnforcingReconcileStatus(reconcileStatus EnforcingReconcileStatus) { m.Status.EnforcingReconcileStatus = reconcileStatus } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // TemplatedEnforcingCRD is the Schema for the templatedenforcingcrds API type TemplatedEnforcingCRD struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec TemplatedEnforcingCRDSpec `json:"spec,omitempty"` Status TemplatedEnforcingCRDStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // TemplatedEnforcingCRDList contains a list of TemplatedEnforcingCRD type TemplatedEnforcingCRDList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []TemplatedEnforcingCRD `json:"items"` } func init() { SchemeBuilder.Register(&TemplatedEnforcingCRD{}, &TemplatedEnforcingCRDList{}) } ================================================ FILE: api/v1alpha1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in ConditionMap) DeepCopyInto(out *ConditionMap) { { in := &in *out = make(ConditionMap, len(*in)) for key, val := range *in { var outVal []v1.Condition if val == nil { (*out)[key] = nil } else { in, out := &val, &outVal *out = make(Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } (*out)[key] = outVal } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionMap. func (in ConditionMap) DeepCopy() ConditionMap { if in == nil { return nil } out := new(ConditionMap) in.DeepCopyInto(out) return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in Conditions) DeepCopyInto(out *Conditions) { { in := &in *out = make(Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Conditions. func (in Conditions) DeepCopy() Conditions { if in == nil { return nil } out := new(Conditions) in.DeepCopyInto(out) return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingCRD) DeepCopyInto(out *EnforcingCRD) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingCRD. func (in *EnforcingCRD) DeepCopy() *EnforcingCRD { if in == nil { return nil } out := new(EnforcingCRD) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *EnforcingCRD) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingCRDList) DeepCopyInto(out *EnforcingCRDList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]EnforcingCRD, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingCRDList. func (in *EnforcingCRDList) DeepCopy() *EnforcingCRDList { if in == nil { return nil } out := new(EnforcingCRDList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *EnforcingCRDList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingCRDSpec) DeepCopyInto(out *EnforcingCRDSpec) { *out = *in if in.Resources != nil { in, out := &in.Resources, &out.Resources *out = make([]LockedResource, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingCRDSpec. func (in *EnforcingCRDSpec) DeepCopy() *EnforcingCRDSpec { if in == nil { return nil } out := new(EnforcingCRDSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingCRDStatus) DeepCopyInto(out *EnforcingCRDStatus) { *out = *in in.EnforcingReconcileStatus.DeepCopyInto(&out.EnforcingReconcileStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingCRDStatus. func (in *EnforcingCRDStatus) DeepCopy() *EnforcingCRDStatus { if in == nil { return nil } out := new(EnforcingCRDStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingPatch) DeepCopyInto(out *EnforcingPatch) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingPatch. func (in *EnforcingPatch) DeepCopy() *EnforcingPatch { if in == nil { return nil } out := new(EnforcingPatch) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *EnforcingPatch) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingPatchList) DeepCopyInto(out *EnforcingPatchList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]EnforcingPatch, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingPatchList. func (in *EnforcingPatchList) DeepCopy() *EnforcingPatchList { if in == nil { return nil } out := new(EnforcingPatchList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *EnforcingPatchList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingPatchSpec) DeepCopyInto(out *EnforcingPatchSpec) { *out = *in if in.Patches != nil { in, out := &in.Patches, &out.Patches *out = make(map[string]PatchSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingPatchSpec. func (in *EnforcingPatchSpec) DeepCopy() *EnforcingPatchSpec { if in == nil { return nil } out := new(EnforcingPatchSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingPatchStatus) DeepCopyInto(out *EnforcingPatchStatus) { *out = *in in.EnforcingReconcileStatus.DeepCopyInto(&out.EnforcingReconcileStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingPatchStatus. func (in *EnforcingPatchStatus) DeepCopy() *EnforcingPatchStatus { if in == nil { return nil } out := new(EnforcingPatchStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnforcingReconcileStatus) DeepCopyInto(out *EnforcingReconcileStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.LockedResourceStatuses != nil { in, out := &in.LockedResourceStatuses, &out.LockedResourceStatuses *out = make(map[string]Conditions, len(*in)) for key, val := range *in { var outVal []v1.Condition if val == nil { (*out)[key] = nil } else { in, out := &val, &outVal *out = make(Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } (*out)[key] = outVal } } if in.LockedPatchStatuses != nil { in, out := &in.LockedPatchStatuses, &out.LockedPatchStatuses *out = make(map[string]ConditionMap, len(*in)) for key, val := range *in { var outVal map[string]Conditions if val == nil { (*out)[key] = nil } else { in, out := &val, &outVal *out = make(ConditionMap, len(*in)) for key, val := range *in { var outVal []v1.Condition if val == nil { (*out)[key] = nil } else { in, out := &val, &outVal *out = make(Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } (*out)[key] = outVal } } (*out)[key] = outVal } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnforcingReconcileStatus. func (in *EnforcingReconcileStatus) DeepCopy() *EnforcingReconcileStatus { if in == nil { return nil } out := new(EnforcingReconcileStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockedResource) DeepCopyInto(out *LockedResource) { *out = *in in.Object.DeepCopyInto(&out.Object) if in.ExcludedPaths != nil { in, out := &in.ExcludedPaths, &out.ExcludedPaths *out = make([]string, len(*in)) copy(*out, *in) } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockedResource. func (in *LockedResource) DeepCopy() *LockedResource { if in == nil { return nil } out := new(LockedResource) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockedResourceTemplate) DeepCopyInto(out *LockedResourceTemplate) { *out = *in if in.ExcludedPaths != nil { in, out := &in.ExcludedPaths, &out.ExcludedPaths *out = make([]string, len(*in)) copy(*out, *in) } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockedResourceTemplate. func (in *LockedResourceTemplate) DeepCopy() *LockedResourceTemplate { if in == nil { return nil } out := new(LockedResourceTemplate) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MyCRD) DeepCopyInto(out *MyCRD) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MyCRD. func (in *MyCRD) DeepCopy() *MyCRD { if in == nil { return nil } out := new(MyCRD) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *MyCRD) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MyCRDList) DeepCopyInto(out *MyCRDList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]MyCRD, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MyCRDList. func (in *MyCRDList) DeepCopy() *MyCRDList { if in == nil { return nil } out := new(MyCRDList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *MyCRDList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MyCRDSpec) DeepCopyInto(out *MyCRDSpec) { *out = *in } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MyCRDSpec. func (in *MyCRDSpec) DeepCopy() *MyCRDSpec { if in == nil { return nil } out := new(MyCRDSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MyCRDStatus) DeepCopyInto(out *MyCRDStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MyCRDStatus. func (in *MyCRDStatus) DeepCopy() *MyCRDStatus { if in == nil { return nil } out := new(MyCRDStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchSpec) DeepCopyInto(out *PatchSpec) { *out = *in if in.SourceObjectRefs != nil { in, out := &in.SourceObjectRefs, &out.SourceObjectRefs *out = make([]SourceObjectReference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } in.TargetObjectRef.DeepCopyInto(&out.TargetObjectRef) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSpec. func (in *PatchSpec) DeepCopy() *PatchSpec { if in == nil { return nil } out := new(PatchSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SourceObjectReference) DeepCopyInto(out *SourceObjectReference) { *out = *in if in.apiResource != nil { in, out := &in.apiResource, &out.apiResource *out = new(v1.APIResource) (*in).DeepCopyInto(*out) } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceObjectReference. func (in *SourceObjectReference) DeepCopy() *SourceObjectReference { if in == nil { return nil } out := new(SourceObjectReference) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetObjectReference) DeepCopyInto(out *TargetObjectReference) { *out = *in if in.LabelSelector != nil { in, out := &in.LabelSelector, &out.LabelSelector *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } if in.AnnotationSelector != nil { in, out := &in.AnnotationSelector, &out.AnnotationSelector *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } if in.apiResource != nil { in, out := &in.apiResource, &out.apiResource *out = new(v1.APIResource) (*in).DeepCopyInto(*out) } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetObjectReference. func (in *TargetObjectReference) DeepCopy() *TargetObjectReference { if in == nil { return nil } out := new(TargetObjectReference) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplatedEnforcingCRD) DeepCopyInto(out *TemplatedEnforcingCRD) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplatedEnforcingCRD. func (in *TemplatedEnforcingCRD) DeepCopy() *TemplatedEnforcingCRD { if in == nil { return nil } out := new(TemplatedEnforcingCRD) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *TemplatedEnforcingCRD) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplatedEnforcingCRDList) DeepCopyInto(out *TemplatedEnforcingCRDList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]TemplatedEnforcingCRD, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplatedEnforcingCRDList. func (in *TemplatedEnforcingCRDList) DeepCopy() *TemplatedEnforcingCRDList { if in == nil { return nil } out := new(TemplatedEnforcingCRDList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *TemplatedEnforcingCRDList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplatedEnforcingCRDSpec) DeepCopyInto(out *TemplatedEnforcingCRDSpec) { *out = *in if in.Templates != nil { in, out := &in.Templates, &out.Templates *out = make([]LockedResourceTemplate, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplatedEnforcingCRDSpec. func (in *TemplatedEnforcingCRDSpec) DeepCopy() *TemplatedEnforcingCRDSpec { if in == nil { return nil } out := new(TemplatedEnforcingCRDSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplatedEnforcingCRDStatus) DeepCopyInto(out *TemplatedEnforcingCRDStatus) { *out = *in in.EnforcingReconcileStatus.DeepCopyInto(&out.EnforcingReconcileStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplatedEnforcingCRDStatus. func (in *TemplatedEnforcingCRDStatus) DeepCopy() *TemplatedEnforcingCRDStatus { if in == nil { return nil } out := new(TemplatedEnforcingCRDStatus) in.DeepCopyInto(out) return out } ================================================ FILE: ci.Dockerfile ================================================ FROM registry.access.redhat.com/ubi8/ubi-minimal WORKDIR / COPY bin/manager . USER 65532:65532 ENTRYPOINT ["/manager"] ================================================ FILE: config/certmanager/certificate.yaml ================================================ # The following manifests contain a self-signed issuer CR and a certificate CR. # More document can be found at https://docs.cert-manager.io # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for # breaking changes apiVersion: cert-manager.io/v1alpha2 kind: Issuer metadata: name: selfsigned-issuer namespace: system spec: selfSigned: {} --- apiVersion: cert-manager.io/v1alpha2 kind: Certificate metadata: name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize dnsNames: - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize ================================================ FILE: config/certmanager/kustomization.yaml ================================================ resources: - certificate.yaml configurations: - kustomizeconfig.yaml ================================================ FILE: config/certmanager/kustomizeconfig.yaml ================================================ # This configuration is for teaching kustomize how to update name ref and var substitution nameReference: - kind: Issuer group: cert-manager.io fieldSpecs: - kind: Certificate group: cert-manager.io path: spec/issuerRef/name varReference: - kind: Certificate group: cert-manager.io path: spec/commonName - kind: Certificate group: cert-manager.io path: spec/dnsNames ================================================ FILE: config/crd/bases/operator-utils.example.io_enforcingcrds.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.0 creationTimestamp: null name: enforcingcrds.operator-utils.example.io spec: group: operator-utils.example.io names: kind: EnforcingCRD listKind: EnforcingCRDList plural: enforcingcrds singular: enforcingcrd scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: EnforcingCRD is the Schema for the enforcingcrds API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: EnforcingCRDSpec defines the desired state of EnforcingCRD properties: resources: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html Resources is a list of resource manifests that should be locked into the specified configuration' items: description: LockedResource represents a resource to be enforced in a LockedResourceController and can be used in a API specification properties: excludedPaths: description: ExludedPaths are a set of json paths that need not be considered by the LockedResourceReconciler items: type: string type: array x-kubernetes-list-type: set object: description: Object is a yaml representation of an API resource type: object required: - object type: object type: array x-kubernetes-list-type: atomic type: object status: description: EnforcingCRDStatus defines the observed state of EnforcingCRD properties: conditions: description: ReconcileStatus this is the general status of the main reconciler items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lockedPatchStatuses: additionalProperties: additionalProperties: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. \ For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array type: object description: LockedResourceStatuses contains the reconcile status for each of the managed resources type: object lockedResourceStatuses: additionalProperties: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. \ For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array description: LockedResourceStatuses contains the reconcile status for each of the managed resources type: object type: object type: object served: true storage: true subresources: status: {} ================================================ FILE: config/crd/bases/operator-utils.example.io_enforcingpatches.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.0 creationTimestamp: null name: enforcingpatches.operator-utils.example.io spec: group: operator-utils.example.io names: kind: EnforcingPatch listKind: EnforcingPatchList plural: enforcingpatches singular: enforcingpatch scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: EnforcingPatch is the Schema for the enforcingpatches API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: EnforcingPatchSpec defines the desired state of EnforcingPatch properties: patches: additionalProperties: description: Patch describes a patch to be enforced at runtime properties: patchTemplate: description: PatchTemplate is a go template that will be resolved using the SourceObjectRefs as parameters. The result must be a valid patch based on the pacth type and the target object. type: string patchType: description: PatchType is the type of patch to be applied, one of "application/json-patch+json"'"application/merge-patch+json","application/strategic-merge-patch+json","application/apply-patch+yaml" default:="application/strategic-merge-patch+json" enum: - application/json-patch+json - application/merge-patch+json - application/strategic-merge-patch+json - application/apply-patch+yaml type: string sourceObjectRefs: description: 'SourceObjectRefs is an arrays of refereces to source objects that will be used as input for the template processing. These refernces must resolve to single instance. The resolution rule is as follows (+ present, - absent): the King and APIVersion field are mandatory -Namespace +Name: resolves to cluster-level object . If Kind is namespaced, this results in an error. -Namespace -Name: results in an error Name manespaces Namespace are evaluated as golang templates with the input of the template being the target object. When selecting multiple target, this allows for having specific source objects for each target. ResourceVersion and UID are always ignored If FieldPath is specified, the restuned object is calculated from the path, so for example if FieldPath=.spec, the only the spec portion of the object is returned. The target object is always added as element zero of the array of the SourceObjectRefs' items: properties: apiVersion: description: API version of the referent. type: string fieldPath: description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.' type: string kind: description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string name: description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string namespace: description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' type: string type: object type: array x-kubernetes-list-type: atomic targetObjectRef: description: 'TargetObjectRef is a reference to the object to which the pacth should be applied. the King and APIVersion field are mandatory the Name and Namespace field have the following meaning (+ present, - absent) -Namespace +Name: apply the patch to the cluster-level object . If Kind is namespaced, this results in an error. -Namespace -Name: if the kind is namespaced apply the patch to all of the objects in all of the namespaces. If the kind is not namespaced, apply the patch to all of the cluster level objects. The lable selector can be used to further filter the selected objects.' properties: annotationSelector: description: AnnotationSelector selects objects by label properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object apiVersion: description: API version of the referent. type: string kind: description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string labelSelector: description: LabelSelector selects objects by label properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. items: description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. properties: key: description: key is the label key that the selector applies to. type: string operator: description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. type: string values: description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. items: type: string type: array required: - key - operator type: object type: array matchLabels: additionalProperties: type: string description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object type: object name: description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string namespace: description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' type: string type: object type: object description: Patches is a list of pacthes that should be encforced at runtime. type: object type: object status: description: EnforcingPatchStatus defines the observed state of EnforcingPatch properties: conditions: description: ReconcileStatus this is the general status of the main reconciler items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lockedPatchStatuses: additionalProperties: additionalProperties: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. \ For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array type: object description: LockedResourceStatuses contains the reconcile status for each of the managed resources type: object lockedResourceStatuses: additionalProperties: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. \ For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array description: LockedResourceStatuses contains the reconcile status for each of the managed resources type: object type: object type: object served: true storage: true subresources: status: {} ================================================ FILE: config/crd/bases/operator-utils.example.io_mycrds.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.0 creationTimestamp: null name: mycrds.operator-utils.example.io spec: group: operator-utils.example.io names: kind: MyCRD listKind: MyCRDList plural: mycrds singular: mycrd scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: MyCRD is the Schema for the mycrds API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: MyCRDSpec defines the desired state of MyCRD properties: error: type: boolean initialized: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html' type: boolean valid: type: boolean required: - error - initialized - valid type: object status: description: MyCRDStatus defines the observed state of MyCRD properties: conditions: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map type: object type: object served: true storage: true subresources: status: {} ================================================ FILE: config/crd/bases/operator-utils.example.io_templatedenforcingcrds.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.0 creationTimestamp: null name: templatedenforcingcrds.operator-utils.example.io spec: group: operator-utils.example.io names: kind: TemplatedEnforcingCRD listKind: TemplatedEnforcingCRDList plural: templatedenforcingcrds singular: templatedenforcingcrd scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: TemplatedEnforcingCRD is the Schema for the templatedenforcingcrds API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: TemplatedEnforcingCRDSpec defines the desired state of TemplatedEnforcingCRD properties: templates: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html' items: description: LockedResourceTemplate represents a resource template in go language to be enforced in a LockedResourceController and can be used in a API specification properties: excludedPaths: description: ExludedPaths are a set of json paths that need not be considered by the LockedResourceReconciler items: type: string type: array x-kubernetes-list-type: set objectTemplate: description: ObjectTemplate is a goland template. Whne processed, it must resolve to a yaml representation of an API resource type: string required: - objectTemplate type: object type: array x-kubernetes-list-type: atomic type: object status: description: TemplatedEnforcingCRDStatus defines the observed state of TemplatedEnforcingCRD properties: conditions: description: ReconcileStatus this is the general status of the main reconciler items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lockedPatchStatuses: additionalProperties: additionalProperties: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. \ For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array type: object description: LockedResourceStatuses contains the reconcile status for each of the managed resources type: object lockedResourceStatuses: additionalProperties: items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. \ For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array description: LockedResourceStatuses contains the reconcile status for each of the managed resources type: object type: object type: object served: true storage: true subresources: status: {} ================================================ FILE: config/crd/kustomization.yaml ================================================ # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: - bases/operator-utils.example.io_mycrds.yaml - bases/operator-utils.example.io_enforcingcrds.yaml - bases/operator-utils.example.io_enforcingpatches.yaml - bases/operator-utils.example.io_templatedenforcingcrds.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_mycrds.yaml #- patches/webhook_in_enforcingcrds.yaml #- patches/webhook_in_enforcingpatches.yaml #- patches/webhook_in_templatedenforcingcrds.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_mycrds.yaml #- patches/cainjection_in_enforcingcrds.yaml #- patches/cainjection_in_enforcingpatches.yaml #- patches/cainjection_in_templatedenforcingcrds.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: - kustomizeconfig.yaml ================================================ FILE: config/crd/kustomizeconfig.yaml ================================================ # This file is for teaching kustomize how to substitute name and namespace reference in CRD nameReference: - kind: Service version: v1 fieldSpecs: - kind: CustomResourceDefinition group: apiextensions.k8s.io path: spec/conversion/webhookClientConfig/service/name namespace: - kind: CustomResourceDefinition group: apiextensions.k8s.io path: spec/conversion/webhookClientConfig/service/namespace create: false varReference: - path: metadata/annotations ================================================ FILE: config/crd/patches/cainjection_in_enforcingcrds.yaml ================================================ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: enforcingcrds.operator-utils.example.io ================================================ FILE: config/crd/patches/cainjection_in_enforcingpatches.yaml ================================================ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: enforcingpatches.operator-utils.example.io ================================================ FILE: config/crd/patches/cainjection_in_mycrds.yaml ================================================ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: mycrds.operator-utils.example.io ================================================ FILE: config/crd/patches/cainjection_in_templatedenforcingcrds.yaml ================================================ # The following patch adds a directive for certmanager to inject CA into the CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: templatedenforcingcrds.operator-utils.example.io ================================================ FILE: config/crd/patches/webhook_in_enforcingcrds.yaml ================================================ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: enforcingcrds.operator-utils.example.io spec: conversion: strategy: Webhook webhookClientConfig: # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) caBundle: Cg== service: namespace: system name: webhook-service path: /convert ================================================ FILE: config/crd/patches/webhook_in_enforcingpatches.yaml ================================================ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: enforcingpatches.operator-utils.example.io spec: conversion: strategy: Webhook webhookClientConfig: # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) caBundle: Cg== service: namespace: system name: webhook-service path: /convert ================================================ FILE: config/crd/patches/webhook_in_mycrds.yaml ================================================ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: mycrds.operator-utils.example.io spec: conversion: strategy: Webhook webhookClientConfig: # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) caBundle: Cg== service: namespace: system name: webhook-service path: /convert ================================================ FILE: config/crd/patches/webhook_in_templatedenforcingcrds.yaml ================================================ # The following patch enables conversion webhook for CRD # CRD conversion requires k8s 1.13 or later. apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: templatedenforcingcrds.operator-utils.example.io spec: conversion: strategy: Webhook webhookClientConfig: # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) caBundle: Cg== service: namespace: system name: webhook-service path: /convert ================================================ FILE: config/default/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: operator-utils # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: operator-utils- # Labels to add to all resources and selectors. #commonLabels: # someName: someValue bases: - ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. - ../prometheus patchesStrategicMerge: # Protect the /metrics endpoint by putting it behind auth. # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. - manager_auth_proxy_patch.yaml # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection #- webhookcainjection_patch.yaml # the following config is for teaching kustomize how to do var substitution vars: - name: METRICS_SERVICE_NAME objref: kind: Service version: v1 name: controller-manager-metrics - name: METRICS_SERVICE_NAMESPACE objref: kind: Service version: v1 name: controller-manager-metrics fieldref: fieldpath: metadata.namespace # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR # objref: # kind: Certificate # group: cert-manager.io # version: v1alpha2 # name: serving-cert # this name should match the one in certificate.yaml # fieldref: # fieldpath: metadata.namespace #- name: CERTIFICATE_NAME # objref: # kind: Certificate # group: cert-manager.io # version: v1alpha2 # name: serving-cert # this name should match the one in certificate.yaml #- name: SERVICE_NAMESPACE # namespace of the service # objref: # kind: Service # version: v1 # name: webhook-service # fieldref: # fieldpath: metadata.namespace #- name: SERVICE_NAME # objref: # kind: Service # version: v1 # name: webhook-service ================================================ FILE: config/default/manager_auth_proxy_patch.yaml ================================================ # This patch inject a sidecar container which is a HTTP proxy for the # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system spec: template: spec: containers: - name: kube-rbac-proxy image: quay.io/coreos/kube-rbac-proxy:v0.11.0 args: - "--secure-listen-address=0.0.0.0:8443" - "--upstream=http://127.0.0.1:8080/" - "--logtostderr=true" - "--v=0" - "--tls-cert-file=/etc/certs/tls/tls.crt" - "--tls-private-key-file=/etc/certs/tls/tls.key" volumeMounts: - mountPath: /etc/certs/tls name: tls-cert ports: - containerPort: 8443 name: https resources: limits: cpu: 500m memory: 128Mi requests: cpu: 5m memory: 64Mi - name: manager args: - "--metrics-addr=127.0.0.1:8080" - "--enable-leader-election" volumes: - name: tls-cert secret: defaultMode: 420 secretName: operator-utils-operator-certs ================================================ FILE: config/default/manager_webhook_patch.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system spec: template: spec: containers: - name: manager ports: - containerPort: 9443 name: webhook-server protocol: TCP volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert readOnly: true volumes: - name: cert secret: defaultMode: 420 secretName: webhook-server-cert ================================================ FILE: config/default/webhookcainjection_patch.yaml ================================================ # This patch add annotation to admission webhook config and # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) --- apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) ================================================ FILE: config/helmchart/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: config/helmchart/Chart.yaml.tpl ================================================ apiVersion: v1 name: operator-utils version: ${version} appVersion: ${version} description: Helm chart that deploys operator-utils keywords: - volume - storage - csi - expansion - monitoring sources: - https://github.com/redhat-cop/operator-utils engine: gotpl ================================================ FILE: config/helmchart/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: release-namespace # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: operator-utils- # Labels to add to all resources and selectors. #commonLabels: # someName: someValue bases: - ../rbac - ../prometheus vars: - name: METRICS_SERVICE_NAME objref: kind: Service version: v1 name: controller-manager-metrics - name: METRICS_SERVICE_NAMESPACE objref: kind: Service version: v1 name: controller-manager-metrics fieldref: fieldpath: metadata.namespace ================================================ FILE: config/helmchart/templates/_helpers.tpl ================================================ {{/* vim: set filetype=mustache: */}} {{/* Expand the name of the chart. */}} {{- define "operator-utils.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "operator-utils.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "operator-utils.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "operator-utils.labels" -}} helm.sh/chart: {{ include "operator-utils.chart" . }} {{ include "operator-utils.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "operator-utils.selectorLabels" -}} app.kubernetes.io/name: {{ include "operator-utils.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "operator-utils.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "operator-utils.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ================================================ FILE: config/helmchart/templates/manager.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "operator-utils.fullname" . }} labels: {{- include "operator-utils.labels" . | nindent 4 }} operator: operator-utils-operator spec: selector: matchLabels: {{- include "operator-utils.selectorLabels" . | nindent 6 }} replicas: {{ .Values.replicaCount }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "operator-utils.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: operator-utils-controller-manager containers: - args: - --secure-listen-address=0.0.0.0:8443 - --upstream=http://127.0.0.1:8080/ - --logtostderr=true - --tls-cert-file=/etc/certs/tls/tls.crt - --tls-private-key-file=/etc/certs/tls/tls.key - --v=10 image: "{{ .Values.kube_rbac_proxy.image.repository }}:{{ .Values.kube_rbac_proxy.image.tag }}" name: kube-rbac-proxy ports: - containerPort: 8443 name: https volumeMounts: - mountPath: /etc/certs/tls name: tls-cert imagePullPolicy: {{ .Values.kube_rbac_proxy.image.pullPolicy }} resources: {{- toYaml .Values.kube_rbac_proxy.resources | nindent 10 }} - command: - /manager args: - --enable-leader-election image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} name: {{ .Chart.Name }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} volumes: - name: tls-cert secret: defaultMode: 420 secretName: operator-utils-operator-certs ================================================ FILE: config/helmchart/values.yaml.tpl ================================================ # Default values for helm-try. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 image: repository: ${image_repo} pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: v${version} imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" podAnnotations: {} resources: limits: cpu: 100m memory: 30Mi requests: cpu: 100m memory: 20Mi nodeSelector: {} tolerations: [] affinity: {} kube_rbac_proxy: image: repository: quay.io/coreos/kube-rbac-proxy pullPolicy: IfNotPresent tag: v0.5.0 resources: requests: cpu: 100m memory: 20Mi ================================================ FILE: config/local-development/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: operator-utils-operator-local # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: operator-utils-operator- # Labels to add to all resources and selectors. #commonLabels: # someName: someValue bases: - ../rbac ================================================ FILE: config/manager/kustomization.yaml ================================================ resources: - manager.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller newName: quay.io/raffaelespazzoli/operator-utils newTag: latest ================================================ FILE: config/manager/manager.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: operator: operator-utils-operator name: system --- apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system labels: operator: operator-utils-operator spec: selector: matchLabels: operator: operator-utils-operator replicas: 1 template: metadata: labels: operator: operator-utils-operator spec: serviceAccountName: controller-manager containers: - command: - /manager args: - --enable-leader-election image: controller:latest name: manager resources: limits: cpu: 100m memory: 30Mi requests: cpu: 100m memory: 20Mi terminationGracePeriodSeconds: 10 ================================================ FILE: config/manifests/bases/operator-utils.clusterserviceversion.yaml ================================================ apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: annotations: alm-examples: '[]' capabilities: Basic Install operatorframework.io/suggested-namespace: operator-utils-operator operators.operatorframework.io/builder: operator-sdk-v1.2.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v2 name: operator-utils.v0.0.0 namespace: placeholder spec: apiservicedefinitions: {} customresourcedefinitions: owned: - description: EnforcingCRD is the Schema for the enforcingcrds API displayName: Enforcing CRD kind: EnforcingCRD name: enforcingcrds.operator-utils.example.io version: v1alpha1 - description: EnforcingPatch is the Schema for the enforcingpatches API displayName: Enforcing Patch kind: EnforcingPatch name: enforcingpatches.operator-utils.example.io version: v1alpha1 - description: MyCRD is the Schema for the mycrds API displayName: My CRD kind: MyCRD name: mycrds.operator-utils.example.io version: v1alpha1 description: operator-utils displayName: operator-utils icon: - base64data: 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 mediatype: image/png install: spec: deployments: null strategy: "" installModes: - supported: false type: OwnNamespace - supported: false type: SingleNamespace - supported: false type: MultiNamespace - supported: true type: AllNamespaces keywords: - utils links: - name: Operator Utils url: https://operator-utils.domain maintainers: - email: raffaele.spazzoli@gmail.com' name: '''raffaele spazzoli' maturity: alpha provider: name: Red Hat Community of Practice version: 0.1.0 ================================================ FILE: config/manifests/kustomization.yaml ================================================ resources: - ../default - ../samples - ../scorecard ================================================ FILE: config/prometheus/kustomization.yaml ================================================ resources: - monitor.yaml configurations: - kustomizeconfig.yaml ================================================ FILE: config/prometheus/kustomizeconfig.yaml ================================================ --- varReference: - path: spec/endpoints/tlsConfig/serverName kind: ServiceMonitor ================================================ FILE: config/prometheus/monitor.yaml ================================================ # Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: operator: operator-utils-operator name: controller-manager-metrics-monitor namespace: system spec: endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token interval: 30s port: https scheme: https tlsConfig: caFile: /etc/prometheus/configmaps/serving-certs-ca-bundle/service-ca.crt serverName: $(METRICS_SERVICE_NAME).$(METRICS_SERVICE_NAMESPACE).svc selector: matchLabels: operator: operator-utils-operator ================================================ FILE: config/rbac/auth_proxy_client_clusterrole.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: - nonResourceURLs: ["/metrics"] verbs: ["get"] ================================================ FILE: config/rbac/auth_proxy_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: proxy-role rules: - apiGroups: ["authentication.k8s.io"] resources: - tokenreviews verbs: ["create"] - apiGroups: ["authorization.k8s.io"] resources: - subjectaccessreviews verbs: ["create"] ================================================ FILE: config/rbac/auth_proxy_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: proxy-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: proxy-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: config/rbac/auth_proxy_service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: operator: operator-utils-operator annotations: service.alpha.openshift.io/serving-cert-secret-name: operator-utils-operator-certs name: controller-manager-metrics namespace: system spec: ports: - name: https port: 8443 targetPort: https selector: operator: operator-utils-operator ================================================ FILE: config/rbac/enforcingcrd_editor_role.yaml ================================================ # permissions for end users to edit enforcingcrds. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: enforcingcrd-editor-role rules: - apiGroups: - operator-utils.example.io resources: - enforcingcrds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - enforcingcrds/status verbs: - get ================================================ FILE: config/rbac/enforcingcrd_viewer_role.yaml ================================================ # permissions for end users to view enforcingcrds. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: enforcingcrd-viewer-role rules: - apiGroups: - operator-utils.example.io resources: - enforcingcrds verbs: - get - list - watch - apiGroups: - operator-utils.example.io resources: - enforcingcrds/status verbs: - get ================================================ FILE: config/rbac/enforcingpatch_editor_role.yaml ================================================ # permissions for end users to edit enforcingpatches. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: enforcingpatch-editor-role rules: - apiGroups: - operator-utils.example.io resources: - enforcingpatches verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - enforcingpatches/status verbs: - get ================================================ FILE: config/rbac/enforcingpatch_viewer_role.yaml ================================================ # permissions for end users to view enforcingpatches. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: enforcingpatch-viewer-role rules: - apiGroups: - operator-utils.example.io resources: - enforcingpatches verbs: - get - list - watch - apiGroups: - operator-utils.example.io resources: - enforcingpatches/status verbs: - get ================================================ FILE: config/rbac/kustomization.yaml ================================================ resources: - role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # Comment the following 4 lines if you want to disable # the auth proxy (https://github.com/brancz/kube-rbac-proxy) # which protects your /metrics endpoint. - auth_proxy_service.yaml - auth_proxy_role.yaml - auth_proxy_role_binding.yaml - auth_proxy_client_clusterrole.yaml ================================================ FILE: config/rbac/leader_election_role.yaml ================================================ # permissions to do leader election. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: leader-election-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - configmaps/status verbs: - get - update - patch - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: config/rbac/leader_election_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: leader-election-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: config/rbac/mycrd_editor_role.yaml ================================================ # permissions for end users to edit mycrds. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: mycrd-editor-role rules: - apiGroups: - operator-utils.example.io resources: - mycrds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - mycrds/status verbs: - get ================================================ FILE: config/rbac/mycrd_viewer_role.yaml ================================================ # permissions for end users to view mycrds. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: mycrd-viewer-role rules: - apiGroups: - operator-utils.example.io resources: - mycrds verbs: - get - list - watch - apiGroups: - operator-utils.example.io resources: - mycrds/status verbs: - get ================================================ FILE: config/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null name: manager-role rules: - apiGroups: - '*' resources: - '*' verbs: - '*' - apiGroups: - operator-utils.example.io resources: - enforcingcrds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - enforcingcrds/status verbs: - get - patch - update - apiGroups: - operator-utils.example.io resources: - enforcingpatches verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - enforcingpatches/status verbs: - get - patch - update - apiGroups: - operator-utils.example.io resources: - mycrds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - mycrds/status verbs: - get - patch - update - apiGroups: - operator-utils.example.io resources: - templatedenforcingcrds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - templatedenforcingcrds/status verbs: - get - patch - update ================================================ FILE: config/rbac/role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: config/rbac/templatedenforcingcrd_editor_role.yaml ================================================ # permissions for end users to edit templatedenforcingcrds. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: templatedenforcingcrd-editor-role rules: - apiGroups: - operator-utils.example.io resources: - templatedenforcingcrds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - operator-utils.example.io resources: - templatedenforcingcrds/status verbs: - get ================================================ FILE: config/rbac/templatedenforcingcrd_viewer_role.yaml ================================================ # permissions for end users to view templatedenforcingcrds. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: templatedenforcingcrd-viewer-role rules: - apiGroups: - operator-utils.example.io resources: - templatedenforcingcrds verbs: - get - list - watch - apiGroups: - operator-utils.example.io resources: - templatedenforcingcrds/status verbs: - get ================================================ FILE: config/samples/kustomization.yaml ================================================ ## Append samples you want in your CSV to this file as resources ## resources: - operator-utils_v1alpha1_mycrd.yaml - operator-utils_v1alpha1_enforcingcrd.yaml - operator-utils_v1alpha1_enforcingpatch.yaml - operator-utils_v1alpha1_templatedenforcingcrd.yaml # +kubebuilder:scaffold:manifestskustomizesamples ================================================ FILE: config/samples/operator-utils_v1alpha1_enforcingcrd.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingCRD metadata: name: example-enforcingcrd spec: resources: - object: apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2020-03-30T16:24:08Z" name: test-configmap namespace: test-enforcingcrd data: ciao: ciao - object: apiVersion: route.openshift.io/v1 kind: Route metadata: name: test-route namespace: test-enforcingcrd spec: host: grafana-istio-system.apps.cluster-4cac.sandbox456.opentlc.com tls: termination: reencrypt to: kind: Service name: grafana weight: 100 wildcardPolicy: None ================================================ FILE: config/samples/operator-utils_v1alpha1_enforcingpatch.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingPatch metadata: name: test-field-patch spec: patches: - id: ciao1 targetObjectRef: apiVersion: v1 kind: ServiceAccount name: test namespace: test-enforcing-patch patchTemplate: | metadata: annotations: {{ (index . 0) }}: {{ (index . 1) }} patchType: application/strategic-merge-patch+json sourceObjectRefs: - apiVersion: v1 kind: Namespace name: default fieldPath: $.metadata.uid - apiVersion: v1 kind: ServiceAccount name: default namespace: default fieldPath: $.metadata.uid ================================================ FILE: config/samples/operator-utils_v1alpha1_mycrd.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: MyCRD metadata: name: example-mycrd spec: # Add fields here initialized: false valid: true error: false ================================================ FILE: config/samples/operator-utils_v1alpha1_templatedenforcingcrd.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: TemplatedEnforcingCRD metadata: name: example-enforcingcrd spec: templates: - objectTemplate: | apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2020-03-30T16:24:08Z" name: test-configmap namespace: {{ .Namespace }} data: ciao: ciao - objectTemplate: | apiVersion: route.openshift.io/v1 kind: Route metadata: name: test-route namespace: {{ .Namespace }} spec: host: grafana-istio-system.apps.cluster-4cac.sandbox456.opentlc.com tls: termination: reencrypt to: kind: Service name: grafana weight: 100 wildcardPolicy: None ================================================ FILE: config/scorecard/bases/config.yaml ================================================ apiVersion: scorecard.operatorframework.io/v1alpha3 kind: Configuration metadata: name: config stages: - parallel: true tests: [] ================================================ FILE: config/scorecard/kustomization.yaml ================================================ resources: - bases/config.yaml patchesJson6902: - path: patches/basic.config.yaml target: group: scorecard.operatorframework.io version: v1alpha3 kind: Configuration name: config - path: patches/olm.config.yaml target: group: scorecard.operatorframework.io version: v1alpha3 kind: Configuration name: config # +kubebuilder:scaffold:patchesJson6902 ================================================ FILE: config/scorecard/patches/basic.config.yaml ================================================ - op: add path: /stages/0/tests/- value: entrypoint: - scorecard-test - basic-check-spec image: quay.io/operator-framework/scorecard-test:v1.9.0 labels: suite: basic test: basic-check-spec-test ================================================ FILE: config/scorecard/patches/olm.config.yaml ================================================ - op: add path: /stages/0/tests/- value: entrypoint: - scorecard-test - olm-bundle-validation image: quay.io/operator-framework/scorecard-test:v1.9.0 labels: suite: olm test: olm-bundle-validation-test - op: add path: /stages/0/tests/- value: entrypoint: - scorecard-test - olm-crds-have-validation image: quay.io/operator-framework/scorecard-test:v1.9.0 labels: suite: olm test: olm-crds-have-validation-test - op: add path: /stages/0/tests/- value: entrypoint: - scorecard-test - olm-crds-have-resources image: quay.io/operator-framework/scorecard-test:v1.9.0 labels: suite: olm test: olm-crds-have-resources-test - op: add path: /stages/0/tests/- value: entrypoint: - scorecard-test - olm-spec-descriptors image: quay.io/operator-framework/scorecard-test:v1.9.0 labels: suite: olm test: olm-spec-descriptors-test - op: add path: /stages/0/tests/- value: entrypoint: - scorecard-test - olm-status-descriptors image: quay.io/operator-framework/scorecard-test:v1.9.0 labels: suite: olm test: olm-status-descriptors-test ================================================ FILE: config/webhook/kustomization.yaml ================================================ resources: - manifests.yaml - service.yaml configurations: - kustomizeconfig.yaml ================================================ FILE: config/webhook/kustomizeconfig.yaml ================================================ # the following config is for teaching kustomize where to look at when substituting vars. # It requires kustomize v2.1.0 or newer to work properly. nameReference: - kind: Service version: v1 fieldSpecs: - kind: MutatingWebhookConfiguration group: admissionregistration.k8s.io path: webhooks/clientConfig/service/name - kind: ValidatingWebhookConfiguration group: admissionregistration.k8s.io path: webhooks/clientConfig/service/name namespace: - kind: MutatingWebhookConfiguration group: admissionregistration.k8s.io path: webhooks/clientConfig/service/namespace create: true - kind: ValidatingWebhookConfiguration group: admissionregistration.k8s.io path: webhooks/clientConfig/service/namespace create: true varReference: - path: metadata/annotations ================================================ FILE: config/webhook/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: webhook-service namespace: system spec: ports: - port: 443 targetPort: 9443 selector: control-plane: gitwebhook-operator ================================================ FILE: controllers/enforcingcrd_controller.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "context" "github.com/go-logr/logr" "github.com/scylladb/go-set/strset" "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "github.com/redhat-cop/operator-utils/api/v1alpha1" operatorutilsv1alpha1 "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedpatch" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" ) // EnforcingCRDReconciler reconciles a EnforcingCRD object type EnforcingCRDReconciler struct { lockedresourcecontroller.EnforcingReconciler Log logr.Logger } // +kubebuilder:rbac:groups=operator-utils.example.io,resources=enforcingcrds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator-utils.example.io,resources=enforcingcrds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=*,resources=*,verbs=* func (r *EnforcingCRDReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("enforcingcrd", req.NamespacedName) // Fetch the EnforcingCRD instance instance := &v1alpha1.EnforcingCRD{} err := r.GetClient().Get(context, req.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } if ok := r.IsInitialized(instance); !ok { err := r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } if util.IsBeingDeleted(instance) { if !util.HasFinalizer(instance, controllerName) { return reconcile.Result{}, nil } err := r.manageCleanUpLogic(instance) if err != nil { log.Error(err, "unable to delete instance", "instance", instance) return r.ManageError(context, instance, err) } util.RemoveFinalizer(instance, controllerName) err = r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } lockedResources, err := lockedresource.GetLockedResources(instance.Spec.Resources) if err != nil { log.Error(err, "unable to get locked resources") return r.ManageError(context, instance, err) } err = r.UpdateLockedResources(context, instance, lockedResources, []lockedpatch.LockedPatch{}) if err != nil { log.Error(err, "unable to update locked resources") return r.ManageError(context, instance, err) } return r.ManageSuccess(context, instance) } func (r *EnforcingCRDReconciler) manageCleanUpLogic(instance *v1alpha1.EnforcingCRD) error { err := r.Terminate(instance, true) if err != nil { r.Log.Error(err, "unable to terminate enforcing reconciler for", "instance", instance) return err } return nil } // IsInitialized can be used to check if instance is correctly initialized. // returns false it isn't. func (r *EnforcingCRDReconciler) IsInitialized(instance *v1alpha1.EnforcingCRD) bool { needsUpdate := false for i := range instance.Spec.Resources { currentSet := strset.New(instance.Spec.Resources[i].ExcludedPaths...) if !currentSet.IsEqual(strset.Union(lockedresource.DefaultExcludedPathsSet, currentSet)) { instance.Spec.Resources[i].ExcludedPaths = strset.Union(lockedresource.DefaultExcludedPathsSet, currentSet).List() needsUpdate = true } } if len(instance.Spec.Resources) > 0 && !util.HasFinalizer(instance, controllerName) { util.AddFinalizer(instance, controllerName) needsUpdate = true } if len(instance.Spec.Resources) == 0 && util.HasFinalizer(instance, controllerName) { util.RemoveFinalizer(instance, controllerName) needsUpdate = true } return !needsUpdate } func (r *EnforcingCRDReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&operatorutilsv1alpha1.EnforcingCRD{}). WatchesRawSource(&source.Channel{Source: r.GetStatusChangeChannel()}, &handler.EnqueueRequestForObject{}). Complete(r) } ================================================ FILE: controllers/enforcingpatch_controller.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "context" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "github.com/redhat-cop/operator-utils/api/v1alpha1" operatorutilsv1alpha1 "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedpatch" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" ) // EnforcingPatchReconciler reconciles a EnforcingPatch object type EnforcingPatchReconciler struct { lockedresourcecontroller.EnforcingReconciler Log logr.Logger } // +kubebuilder:rbac:groups=operator-utils.example.io,resources=enforcingpatches,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator-utils.example.io,resources=enforcingpatches/status,verbs=get;update;patch // +kubebuilder:rbac:groups=*,resources=*,verbs=* func (r *EnforcingPatchReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("enforcingpatch", req.NamespacedName) // Fetch the EnforcingPatch instance instance := &v1alpha1.EnforcingPatch{} err := r.GetClient().Get(context, req.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } if ok := r.IsInitialized(instance); !ok { err := r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } lockedPatches, err := lockedpatch.GetLockedPatches(instance.Spec.Patches, r.GetRestConfig(), log) if err != nil { log.Error(err, "unable to get locked patches") return r.ManageError(context, instance, err) } err = r.UpdateLockedResources(context, instance, []lockedresource.LockedResource{}, lockedPatches) if err != nil { log.Error(err, "unable to update locked pacthes") return r.ManageError(context, instance, err) } return r.ManageSuccess(context, instance) } // IsInitialized can be used to check if instance is correctly initialized. // returns false it isn't. func (r *EnforcingPatchReconciler) IsInitialized(instance *v1alpha1.EnforcingPatch) bool { needsUpdate := true for i, patch := range instance.Spec.Patches { if patch.PatchType == "" { patch.PatchType = "application/strategic-merge-patch+json" instance.Spec.Patches[i] = patch needsUpdate = false } } return needsUpdate } func (r *EnforcingPatchReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&operatorutilsv1alpha1.EnforcingPatch{}). WatchesRawSource(&source.Channel{Source: r.GetStatusChangeChannel()}, &handler.EnqueueRequestForObject{}). Complete(r) } ================================================ FILE: controllers/mycrd_controller.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "context" "errors" "github.com/go-logr/logr" "github.com/redhat-cop/operator-utils/api/v1alpha1" operatorutilsv1alpha1 "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const controllerName = "MyCRD_controller" //var log = logf.Log.WithName(controllerName) // MyCRDReconciler reconciles a MyCRD object type MyCRDReconciler struct { util.ReconcilerBase Log logr.Logger } // +kubebuilder:rbac:groups=operator-utils.example.io,resources=mycrds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator-utils.example.io,resources=mycrds/status,verbs=get;update;patch func (r *MyCRDReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("mycrd", req.NamespacedName) // Fetch the MyCRD instance instance := &v1alpha1.MyCRD{} err := r.GetClient().Get(context, req.NamespacedName, instance) if err != nil { if apierrors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } if ok, err := r.IsValid(instance); !ok { return r.ManageError(context, instance, err) } if ok := r.IsInitialized(instance); !ok { err := r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } if util.IsBeingDeleted(instance) { if !util.HasFinalizer(instance, controllerName) { return reconcile.Result{}, nil } err := r.manageCleanUpLogic(instance) if err != nil { log.Error(err, "unable to delete instance", "instance", instance) return r.ManageError(context, instance, err) } util.RemoveFinalizer(instance, controllerName) err = r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } err = r.manageOperatorLogic(instance) if err != nil { return r.ManageError(context, instance, err) } return r.ManageSuccess(context, instance) } func (r *MyCRDReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&operatorutilsv1alpha1.MyCRD{}). Complete(r) } func (r *MyCRDReconciler) IsInitialized(obj metav1.Object) bool { mycrd, ok := obj.(*v1alpha1.MyCRD) if !ok { return false } if mycrd.Spec.Initialized { return true } util.AddFinalizer(mycrd, controllerName) mycrd.Spec.Initialized = true return false } func (r *MyCRDReconciler) IsValid(obj metav1.Object) (bool, error) { mycrd, ok := obj.(*v1alpha1.MyCRD) if !ok { return false, errors.New("not a mycrd object") } if mycrd.Spec.Valid { return true, nil } return false, errors.New("not valid because blah blah") } func (r *MyCRDReconciler) manageCleanUpLogic(mycrd *v1alpha1.MyCRD) error { return nil } func (r *MyCRDReconciler) manageOperatorLogic(mycrd *v1alpha1.MyCRD) error { if mycrd.Spec.Error { return errors.New("error because blah blah") } return nil } ================================================ FILE: controllers/suite_test.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "path/filepath" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" operatorutilsv1alpha1 "github.com/redhat-cop/operator-utils/api/v1alpha1" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) RunSpecsWithDefaultAndCustomReporters(t, "Controller Suite", []Reporter{}) } var _ = BeforeSuite(func(done Done) { //logf.SetLogger(zap.WriteTo(GinkgoWriter)) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, } var err error cfg, err = testEnv.Start() Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) err = operatorutilsv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).ToNot(HaveOccurred()) Expect(k8sClient).ToNot(BeNil()) close(done) }, 60) var _ = AfterSuite(func() { By("tearing down the test environment") err := testEnv.Stop() Expect(err).ToNot(HaveOccurred()) }) ================================================ FILE: controllers/templatedenforcingcrd_controller.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "context" "github.com/go-logr/logr" "github.com/scylladb/go-set/strset" "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "github.com/redhat-cop/operator-utils/api/v1alpha1" operatorutilsv1alpha1 "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedpatch" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" ) // TemplatedEnforcingCRDReconciler reconciles a TemplatedEnforcingCRD object type TemplatedEnforcingCRDReconciler struct { lockedresourcecontroller.EnforcingReconciler Log logr.Logger } // +kubebuilder:rbac:groups=operator-utils.example.io,resources=templatedenforcingcrds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator-utils.example.io,resources=templatedenforcingcrds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=*,resources=*,verbs=* func (r *TemplatedEnforcingCRDReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("templatedenforcingcrd", req.NamespacedName) // Fetch the TemplatedEnforcingCRD instance instance := &v1alpha1.TemplatedEnforcingCRD{} err := r.GetClient().Get(context, req.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } if ok := r.IsInitialized(instance); !ok { err := r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } if util.IsBeingDeleted(instance) { if !util.HasFinalizer(instance, controllerName) { return reconcile.Result{}, nil } err := r.manageCleanUpLogic(instance) if err != nil { log.Error(err, "unable to delete instance", "instance", instance) return r.ManageError(context, instance, err) } util.RemoveFinalizer(instance, controllerName) err = r.GetClient().Update(context, instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) return r.ManageError(context, instance, err) } return reconcile.Result{}, nil } lockedResources, err := lockedresource.GetLockedResourcesFromTemplatesWithRestConfig(instance.Spec.Templates, r.GetRestConfig(), instance) if err != nil { log.Error(err, "unable to get locked resources") return r.ManageError(context, instance, err) } err = r.UpdateLockedResources(context, instance, lockedResources, []lockedpatch.LockedPatch{}) if err != nil { log.Error(err, "unable to update locked resources") return r.ManageError(context, instance, err) } return r.ManageSuccess(context, instance) } // IsInitialized can be used to check if instance is correctly initialized. // returns false it isn't. func (r *TemplatedEnforcingCRDReconciler) IsInitialized(instance *v1alpha1.TemplatedEnforcingCRD) bool { needsUpdate := true for i := range instance.Spec.Templates { currentSet := strset.New(instance.Spec.Templates[i].ExcludedPaths...) if !currentSet.IsEqual(strset.Union(lockedresource.DefaultExcludedPathsSet, currentSet)) { instance.Spec.Templates[i].ExcludedPaths = strset.Union(lockedresource.DefaultExcludedPathsSet, currentSet).List() needsUpdate = false } } if len(instance.Spec.Templates) > 0 && !util.HasFinalizer(instance, controllerName) { util.AddFinalizer(instance, controllerName) needsUpdate = false } if len(instance.Spec.Templates) == 0 && util.HasFinalizer(instance, controllerName) { util.RemoveFinalizer(instance, controllerName) needsUpdate = false } return needsUpdate } func (r *TemplatedEnforcingCRDReconciler) manageCleanUpLogic(instance *v1alpha1.TemplatedEnforcingCRD) error { err := r.Terminate(instance, true) if err != nil { r.Log.Error(err, "unable to terminate enforcing reconciler for", "instance", instance) return err } return nil } func (r *TemplatedEnforcingCRDReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&operatorutilsv1alpha1.TemplatedEnforcingCRD{}). WatchesRawSource(&source.Channel{Source: r.GetStatusChangeChannel()}, &handler.EnqueueRequestForObject{}). Complete(r) } ================================================ FILE: go.mod ================================================ module github.com/redhat-cop/operator-utils go 1.21 require ( github.com/BurntSushi/toml v1.3.2 github.com/Masterminds/sprig/v3 v3.2.3 github.com/evanphx/json-patch v5.7.0+incompatible github.com/go-logr/logr v1.2.4 github.com/hashicorp/go-multierror v1.1.1 github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.10 github.com/pkg/errors v0.9.1 github.com/scylladb/go-set v1.0.2 k8s.io/api v0.28.2 k8s.io/apimachinery v0.28.2 k8s.io/client-go v0.28.2 k8s.io/kubectl v0.28.2 sigs.k8s.io/controller-runtime v0.15.2 sigs.k8s.io/yaml v1.3.0 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.11.0 // indirect golang.org/x/net v0.13.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/term v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.27.2 // indirect k8s.io/cli-runtime v0.28.2 // indirect k8s.io/component-base v0.28.2 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE= github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo= k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ= k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= k8s.io/cli-runtime v0.28.2 h1:64meB2fDj10/ThIMEJLO29a1oujSm0GQmKzh1RtA/uk= k8s.io/cli-runtime v0.28.2/go.mod h1:bTpGOvpdsPtDKoyfG4EG041WIyFZLV9qq4rPlkyYfDA= k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= k8s.io/component-base v0.28.2 h1:Yc1yU+6AQSlpJZyvehm/NkJBII72rzlEsd6MkBQ+G0E= k8s.io/component-base v0.28.2/go.mod h1:4IuQPQviQCg3du4si8GpMrhAIegxpsgPngPRR/zWpzc= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/kubectl v0.28.2 h1:fOWOtU6S0smdNjG1PB9WFbqEIMlkzU5ahyHkc7ESHgM= k8s.io/kubectl v0.28.2/go.mod h1:6EQWTPySF1fn7yKoQZHYf9TPwIl2AygHEcJoxFekr64= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.15.2 h1:9V7b7SDQSJ08IIsJ6CY1CE85Okhp87dyTMNDG0FS7f4= sigs.k8s.io/controller-runtime v0.15.2/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= ================================================ FILE: hack/boilerplate.go.txt ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ ================================================ FILE: main.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "flag" "os" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" operatorutilsv1alpha1 "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/controllers" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller" // +kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(operatorutilsv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } func main() { var metricsAddr string var enableLeaderElection bool flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.Parse() ctrl.SetLogger(zap.New(zap.UseDevMode(true))) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, Port: 9443, LeaderElection: enableLeaderElection, LeaderElectionID: "a0942526.example.io", LeaderElectionResourceLock: "configmaps", }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err = (&controllers.MyCRDReconciler{ ReconcilerBase: util.NewFromManager(mgr, mgr.GetEventRecorderFor("MyCRD_controller")), Log: ctrl.Log.WithName("controllers").WithName("MyCRD"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MyCRD") os.Exit(1) } if err = (&controllers.EnforcingCRDReconciler{ EnforcingReconciler: lockedresourcecontroller.NewFromManager(mgr, "EnforcingCRD_controller", true, false), Log: ctrl.Log.WithName("controllers").WithName("EnforcingCRD"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "EnforcingCRD") os.Exit(1) } if err = (&controllers.EnforcingPatchReconciler{ EnforcingReconciler: lockedresourcecontroller.NewFromManager(mgr, "EnforcingPatch_controller", true, false), Log: ctrl.Log.WithName("controllers").WithName("EnforcingPatch"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "EnforcingPatch") os.Exit(1) } if err = (&controllers.TemplatedEnforcingCRDReconciler{ EnforcingReconciler: lockedresourcecontroller.NewFromManager(mgr, "TemplatedEnforcingCRD_controller", true, false), Log: ctrl.Log.WithName("controllers").WithName("TemplatedEnforcingCRD"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "TemplatedEnforcingCRD") os.Exit(1) } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } ================================================ FILE: pkg/util/apis/conditions.go ================================================ package apis import ( "sort" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ReconcileError = "ReconcileError" const ReconcileErrorReason = "LastReconcileCycleFailed" const ReconcileSuccess = "ReconcileSuccess" const ReconcileSuccessReason = "LastReconcileCycleSucceded" // ConditionsAware represents a CRD type that has been enabled with metav1.Conditions, it can then benefit of a series of utility methods. type ConditionsAware interface { GetConditions() []metav1.Condition SetConditions(conditions []metav1.Condition) } // AddOrReplaceCondition adds or replaces the passed condition in the passed array of conditions func AddOrReplaceCondition(c metav1.Condition, conditions []metav1.Condition) []metav1.Condition { for i, condition := range conditions { if c.Type == condition.Type { conditions[i] = c return conditions } } conditions = append(conditions, c) return conditions } // GetCondition returns the condition with the given type, if it exists. If the condition does not exists it returns false. func GetCondition(conditionType string, conditions []metav1.Condition) (metav1.Condition, bool) { for _, condition := range conditions { if condition.Type == conditionType { return condition, true } } return metav1.Condition{}, false } // GetLastCondition retruns the last condition based on the condition timestamp. if no condition is present it return false. func GetLastCondition(conditions []metav1.Condition) (metav1.Condition, bool) { if len(conditions) == 0 { return metav1.Condition{}, false } //we need to make a copy of the slice copiedConditions := []metav1.Condition{} for _, condition := range conditions { ccondition := condition.DeepCopy() copiedConditions = append(copiedConditions, *ccondition) } sort.Slice(copiedConditions, func(i, j int) bool { return copiedConditions[i].LastTransitionTime.Before(&copiedConditions[j].LastTransitionTime) }) return copiedConditions[len(copiedConditions)-1], true } func IsErrorCondition(condition metav1.Condition) bool { return !(condition.Type == ReconcileSuccess) || (condition.Type == "Initializing") } ================================================ FILE: pkg/util/apis/key.go ================================================ package apis import ( "errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" ) var log = ctrl.Log.WithName("util.api") // GetKeyLong return a unique key for a given object in the pattern of /// // namespace can be null func GetKeyLong(obj metav1.Object) string { robj, ok := obj.(runtime.Object) if !ok { err := errors.New("unable to conver meta.Object to runtime.Object") log.Error(err, "unable to conver meta.Object to runtime.Object", "object", obj) panic(err) } return robj.GetObjectKind().GroupVersionKind().GroupVersion().String() + "/" + robj.GetObjectKind().GroupVersionKind().Kind + "/" + obj.GetNamespace() + "/" + obj.GetName() } // GetKeyShort return a unique key for a given object in the pattern of // // namespace can be null func GetKeyShort(obj metav1.Object) string { return obj.GetNamespace() + "/" + obj.GetName() } ================================================ FILE: pkg/util/crud/crudutils.go ================================================ package crud import ( "context" "text/template" "github.com/redhat-cop/operator-utils/pkg/util/templates" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" ) // CreateOrUpdateResource creates a resource if it doesn't exist, and updates (overwrites it), if it exist // if owner is not nil, the owner field os set // if namespace is not "", the namespace field of the object is overwritten with the passed value // requires a context with log and client func CreateOrUpdateResource(context context.Context, owner client.Object, namespace string, obj client.Object) error { log := log.FromContext(context) client := context.Value("client").(client.Client) if owner != nil { _ = controllerutil.SetControllerReference(owner, obj, client.Scheme()) } if namespace != "" { obj.SetNamespace(namespace) } obj2 := &unstructured.Unstructured{} obj2.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) err := client.Get(context, types.NamespacedName{ Namespace: obj.GetNamespace(), Name: obj.GetName(), }, obj2) if apierrors.IsNotFound(err) { err = client.Create(context, obj) if err != nil { log.Error(err, "unable to create object", "object", obj) return err } return nil } if err == nil { obj.SetResourceVersion(obj2.GetResourceVersion()) err = client.Update(context, obj) if err != nil { log.Error(err, "unable to update object", "object", obj) return err } return nil } log.Error(err, "unable to lookup object", "object", obj) return err } // CreateOrUpdateResources operates as CreateOrUpdate, but on an array of resources // requires a context with log and client func CreateOrUpdateResources(context context.Context, owner client.Object, namespace string, objs []client.Object) error { for _, obj := range objs { err := CreateOrUpdateResource(context, owner, namespace, obj) if err != nil { return err } } return nil } // CreateOrUpdateUnstructuredResources operates as CreateOrUpdate, but on an array of unstructured.Unstructured // requires a context with log and client func CreateOrUpdateUnstructuredResources(context context.Context, owner client.Object, namespace string, objs []unstructured.Unstructured) error { for _, obj := range objs { err := CreateOrUpdateResource(context, owner, namespace, &obj) if err != nil { return err } } return nil } // DeleteResourceIfExists deletes an existing resource. It doesn't fail if the resource does not exist // requires a context with log and client func DeleteResourceIfExists(context context.Context, obj client.Object) error { log := log.FromContext(context) client := context.Value("client").(client.Client) err := client.Delete(context, obj) if err != nil && !apierrors.IsNotFound(err) { log.Error(err, "unable to delete object ", "object", obj) return err } return nil } // DeleteResourcesIfExist operates like DeleteResources, but on an arrays of resources // requires a context with log and client func DeleteResourcesIfExist(context context.Context, objs []client.Object) error { for _, obj := range objs { err := DeleteResourceIfExists(context, obj) if err != nil { return err } } return nil } // DeleteUnstructuredResources operates like DeleteResources, but on an arrays of unstructured.Unstructured // requires a context with log and client func DeleteUnstructuredResources(context context.Context, objs []unstructured.Unstructured) error { for _, obj := range objs { err := DeleteResourceIfExists(context, &obj) if err != nil { return err } } return nil } // CreateResourceIfNotExists create a resource if it doesn't already exists. If the resource exists it is left untouched and the functin does not fails // if owner is not nil, the owner field os set // if namespace is not "", the namespace field of the object is overwritten with the passed value // requires a context with log and client func CreateResourceIfNotExists(context context.Context, owner client.Object, namespace string, obj client.Object) error { log := log.FromContext(context) client := context.Value("client").(client.Client) if owner != nil { _ = controllerutil.SetControllerReference(owner, obj, client.Scheme()) } if namespace != "" { obj.SetNamespace(namespace) } err := client.Create(context, obj) if err != nil && !apierrors.IsAlreadyExists(err) { log.Error(err, "unable to create object ", "object", obj) return err } return nil } // CreateResourcesIfNotExist operates as CreateResourceIfNotExists, but on an array of resources // requires a context with log and client func CreateResourcesIfNotExist(context context.Context, owner client.Object, namespace string, objs []client.Object) error { for _, obj := range objs { err := CreateResourceIfNotExists(context, owner, namespace, obj) if err != nil { return err } } return nil } // CreateUnstructuredResourcesIfNotExist operates as CreateResourceIfNotExists, but on an array of unstructured.Unstructured // requires a context with log and client func CreateUnstructuredResourcesIfNotExist(context context.Context, owner client.Object, namespace string, objs []unstructured.Unstructured) error { for _, obj := range objs { err := CreateResourceIfNotExists(context, owner, namespace, &obj) if err != nil { return err } } return nil } // CreateOrUpdateTemplatedResources processes an initialized template expecting an array of objects as a result and the processes them with the CreateOrUpdate function // requires a context with log and client func CreateOrUpdateTemplatedResources(context context.Context, owner client.Object, namespace string, data interface{}, template *template.Template) error { log := log.FromContext(context) objs, err := templates.ProcessTemplateArray(context, data, template) if err != nil { log.Error(err, "error creating manifest from template") return err } for _, obj := range objs { err = CreateOrUpdateResource(context, owner, namespace, &obj) if err != nil { return err } } return nil } // CreateIfNotExistTemplatedResources processes an initialized template expecting an array of objects as a result and then processes them with the CreateResourceIfNotExists function // requires a context with log and client func CreateIfNotExistTemplatedResources(context context.Context, owner client.Object, namespace string, data interface{}, template *template.Template) error { log := log.FromContext(context) objs, err := templates.ProcessTemplateArray(context, data, template) if err != nil { log.Error(err, "error creating manifest from template") return err } for _, obj := range objs { err = CreateResourceIfNotExists(context, owner, namespace, &obj) if err != nil { return err } } return nil } // DeleteTemplatedResources processes an initialized template expecting an array of objects as a result and then processes them with the Delete function // requires a context with log and client func DeleteTemplatedResources(context context.Context, data interface{}, template *template.Template) error { log := log.FromContext(context) objs, err := templates.ProcessTemplateArray(context, data, template) if err != nil { log.Error(err, "error creating manifest from template") return err } for _, obj := range objs { err = DeleteResourceIfExists(context, &obj) if err != nil { return err } } return nil } ================================================ FILE: pkg/util/discoveryclient/discoveryclientutils.go ================================================ package discoveryclient import ( "context" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/log" ) // GetDiscoveryClient returns a discovery client for the current reconciler // needs context with restConfig func GetDiscoveryClient(context context.Context) (*discovery.DiscoveryClient, error) { restConfig := context.Value("restConfig").(*rest.Config) return discovery.NewDiscoveryClientForConfig(restConfig) } // IsAPIResourceAvailable checks of a give GroupVersionKind is available in the running apiserver // needs context with restConfig and log func IsGVKDefined(context context.Context, GVK schema.GroupVersionKind) (bool, error) { _, found, err := GetAPIResourceForGVK(context, GVK) return found, err } func GetAPIResourceForGVK(context context.Context, GVK schema.GroupVersionKind) (apiresource *v1.APIResource, found bool, err error) { log := log.FromContext(context) discoveryClient, err := GetDiscoveryClient(context) if err != nil { log.Error(err, "Unable to get discovery client") return nil, false, err } // Query for known OpenShift API resource to verify it is available apiResources, err := discoveryClient.ServerResourcesForGroupVersion(GVK.GroupVersion().String()) if err != nil { if apierrors.IsNotFound(err) { return nil, false, nil } log.Error(err, "Unable to retrive resources for", "GVK", GVK) return nil, false, err } for i := range apiResources.APIResources { if apiResources.APIResources[i].Kind == GVK.Kind { return &apiResources.APIResources[i], true, nil } } return nil, false, nil } // IsGVKNamespaced checks whether the passed GVK os namespaced // needs context with restConfig and log func IsGVKNamespaced(context context.Context, GVK schema.GroupVersionKind) (bool, error) { resource, found, err := GetAPIResourceForGVK(context, GVK) if err != nil || !found { return found, err } return resource.Namespaced, nil } // IsUnstructuredDefined checks whether the content of a unstructured is defined in the current cluster // needs context with restConfig and log func IsUnstructuredDefined(context context.Context, obj *unstructured.Unstructured) (bool, error) { return IsGVKDefined(context, obj.GroupVersionKind()) } // IsUnstructuredDefined checks whether the content of a unstructured is defined in the current cluster // needs context with restConfig and log func IsUnstructuredNamespaced(context context.Context, obj *unstructured.Unstructured) (bool, error) { return IsGVKNamespaced(context, obj.GroupVersionKind()) } ================================================ FILE: pkg/util/dynamicclient/dynamicclientutils.go ================================================ package dynamicclient import ( "context" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "k8s.io/client-go/util/jsonpath" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) // GetDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. // needs context with log and restConfig // TODO consider refactoring using apimachinery.RESTClientForGVK in controller-runtime func GetDynamicClientOnUnstructured(context context.Context, obj *unstructured.Unstructured) (dynamic.ResourceInterface, error) { log := log.FromContext(context) apiRes, err := getAPIReourceForGVK(context, obj.GetObjectKind().GroupVersionKind()) if err != nil { log.Error(err, "Unable to get apiresource from unstructured", "unstructured", obj) return nil, err } dc, err := GetDynamicClientForAPIResource(context, apiRes) if err != nil { log.Error(err, "Unable to get namespaceable dynamic client from ", "resource", apiRes) return nil, err } if apiRes.Namespaced { return dc.Namespace(obj.GetNamespace()), nil } return dc, nil } // GetDynamicClientOnAPIResource returns a dynamic client on an APIResource. This client can be further namespaced. // needs context with log and restConfig func GetDynamicClientForAPIResource(context context.Context, resource *metav1.APIResource) (dynamic.NamespaceableResourceInterface, error) { return getDynamicClientForGVR(context, schema.GroupVersionResource{ Group: resource.Group, Version: resource.Version, Resource: resource.Name, }) } func getDynamicClientForGVR(context context.Context, gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error) { log := log.FromContext(context) restConfig := context.Value("restConfig").(*rest.Config) intf, err := dynamic.NewForConfig(restConfig) if err != nil { log.Error(err, "Unable to get dynamic client") return nil, err } res := intf.Resource(gvr) return res, nil } // GetDynamicClientForGVK returns a dynamic client on an gvk type. Also returns whether this reosurce is namespaced. This client can be further namespaced. // needs context with log and restConfig func GetDynamicClientForGVK(context context.Context, gvk schema.GroupVersionKind) (dynamic.NamespaceableResourceInterface, bool, error) { log := log.FromContext(context) apiRes, err := getAPIReourceForGVK(context, gvk) if err != nil { log.Error(err, "unable to get apiresource from", "gvk", gvk) return nil, false, err } nri, err := GetDynamicClientForAPIResource(context, apiRes) if err != nil { log.Error(err, "unable to get dynamic client from", "apires", apiRes) return nil, false, err } return nri, apiRes.Namespaced, nil } func getAPIReourceForGVK(context context.Context, gvk schema.GroupVersionKind) (*metav1.APIResource, error) { res := &metav1.APIResource{} log := log.FromContext(context) restConfig := context.Value("restConfig").(*rest.Config) discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(restConfig) resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { log.Error(err, "unable to retrieve resource list for", "gvk", gvk.GroupVersion().String()) return nil, err } for i := range resList.APIResources { //if a resource contains a "/" it's referencing a subresource. we don't support subresource for now. if resList.APIResources[i].Kind == gvk.Kind && !strings.Contains(resList.APIResources[i].Name, "/") { res = &resList.APIResources[i] res.Group = gvk.Group res.Version = gvk.Version break } } return res, nil } // SetIndexField this function allows to prepare an index field for an objct so that fieldSelector can be used. // It needs a cache object probably obtained via mrg.GetCache() // This is a generic implementation, so it's relatively slow // path should be expressed in the form of .. ... // needs context with log func SetIndexField(context context.Context, cache cache.Cache, obj client.Object, path string) error { log := log.FromContext(context) return cache.IndexField(context, obj, path, func(o client.Object) []string { mapObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) if err != nil { log.Error(err, "unable to convert object to unstructured ", "object", o) return nil } jp := jsonpath.New("fieldPath:" + path) err = jp.Parse("{ $" + path + "}") if err != nil { log.Error(err, "unable to parse ", "fieldPath", path) return nil } values, err := jp.FindResults(mapObj) if err != nil { log.Error(err, "unable to apply ", "jsonpath", jp, " to obj ", mapObj) return nil } result := []string{} if len(values) > 0 { for i := range values[0] { if val, ok := values[0][i].Interface().(string); ok { result = append(result, val) } } } return result }) } ================================================ FILE: pkg/util/finalizer.go ================================================ /* Copyright 2019 Red Hat, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) // IsBeingDeleted returns whether this object has been requested to be deleted func IsBeingDeleted(obj client.Object) bool { return !obj.GetDeletionTimestamp().IsZero() } // HasFinalizer returns whether this object has the passed finalizer // Deprecated use controllerutil.ContainsFinalizer func HasFinalizer(obj client.Object, finalizer string) bool { return controllerutil.ContainsFinalizer(obj, finalizer) } // AddFinalizer adds the passed finalizer this object // Deprecated use controllerutil.AddFinalizer func AddFinalizer(obj client.Object, finalizer string) { controllerutil.AddFinalizer(obj, finalizer) } // RemoveFinalizer removes the passed finalizer from object // Deprecated use controllerutil.RemoveFinalizer func RemoveFinalizer(obj client.Object, finalizer string) { controllerutil.RemoveFinalizer(obj, finalizer) } ================================================ FILE: pkg/util/lockedresourcecontroller/enforcing-reconciler.go ================================================ package lockedresourcecontroller import ( "context" "sync" "github.com/go-logr/logr" "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/apis" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedpatch" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" "github.com/scylladb/go-set/strset" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // EnforcingReconciler is a reconciler designed to as a base type to extend for those operators that compute a set of resources that then need to be kept in place (i.e. enforced) // the enforcing piece is taken care for, an implementor would just need to take care of the logic that computes the resources to be enforced. type EnforcingReconciler struct { util.ReconcilerBase lockedResourceManagers map[string]*LockedResourceManager statusChange chan event.GenericEvent lockedResourceManagersMutex sync.Mutex clusterWatchers bool log logr.Logger returnOnlyFailingStatuses bool } // NewEnforcingReconciler creates a new EnforcingReconciler // clusterWatcher determines whether the created watchers should be at the cluster level or namespace level. // this affects the kind of permissions needed to run the controller // also creating multiple namespace level permissions can create performance issue as one watch per object type per namespace is opened to the API server, if in doubt pass true here. func NewEnforcingReconciler(client client.Client, scheme *runtime.Scheme, restConfig *rest.Config, apireader client.Reader, recorder record.EventRecorder, clusterWatchers bool, returnOnlyFailingStatuses bool) EnforcingReconciler { return EnforcingReconciler{ ReconcilerBase: util.NewReconcilerBase(client, scheme, restConfig, recorder, apireader), lockedResourceManagers: map[string]*LockedResourceManager{}, statusChange: make(chan event.GenericEvent), lockedResourceManagersMutex: sync.Mutex{}, clusterWatchers: clusterWatchers, log: ctrl.Log.WithName("enforcing-reconciler"), returnOnlyFailingStatuses: returnOnlyFailingStatuses, } } func NewFromManager(mgr manager.Manager, recorderName string, clusterWatchers bool, returnOnlyFailingStatuses bool) EnforcingReconciler { return NewEnforcingReconciler(mgr.GetClient(), mgr.GetScheme(), mgr.GetConfig(), mgr.GetAPIReader(), mgr.GetEventRecorderFor(recorderName), clusterWatchers, returnOnlyFailingStatuses) } // GetStatusChangeChannel returns the channel through which status change events can be received func (er *EnforcingReconciler) GetStatusChangeChannel() <-chan event.GenericEvent { return er.statusChange } func (er *EnforcingReconciler) removeLockedResourceManager(instance client.Object) { er.lockedResourceManagersMutex.Lock() defer er.lockedResourceManagersMutex.Unlock() delete(er.lockedResourceManagers, apis.GetKeyShort(instance)) } func (er *EnforcingReconciler) getLockedResourceManager(instance client.Object) (*LockedResourceManager, error) { er.lockedResourceManagersMutex.Lock() defer er.lockedResourceManagersMutex.Unlock() lockedResourceManager, ok := er.lockedResourceManagers[apis.GetKeyShort(instance)] if !ok { lockedResourceManager, err := NewLockedResourceManager(er.GetRestConfig(), manager.Options{}, instance, er.statusChange, er.clusterWatchers) if err != nil { er.log.Error(err, "unable to create LockedResourceManager") return &LockedResourceManager{}, err } er.lockedResourceManagers[apis.GetKeyShort(instance)] = &lockedResourceManager return &lockedResourceManager, nil } return lockedResourceManager, nil } // UpdateLockedResources will do the following: // 1. initialize or retrieve the LockedResourceManager related to the passed parent resource // 2. compare the currently enforced resources with the one passed as parameters and then // a. return immediately if they are the same // b. restart the LockedResourceManager if they don't match func (er *EnforcingReconciler) UpdateLockedResources(context context.Context, instance client.Object, lockedResources []lockedresource.LockedResource, lockedPatches []lockedpatch.LockedPatch) error { return er.UpdateLockedResourcesWithRestConfig(context, instance, lockedResources, lockedPatches, er.GetRestConfig()) } // UpdateLockedResourcesWithRestConfig will do the following: // 1. initialize or retrieve the LockedResourceManager related to the passed parent resource // 2. compare the currently enforced resources with the one passed as parameters and then // a. return immediately if they are the same // b. restart the LockedResourceManager if they don't match // // this variant allows passing a rest config func (er *EnforcingReconciler) UpdateLockedResourcesWithRestConfig(context context.Context, instance client.Object, lockedResources []lockedresource.LockedResource, lockedPatches []lockedpatch.LockedPatch, config *rest.Config) error { lockedResourceManager, err := er.getLockedResourceManager(instance) if err != nil { er.log.Error(err, "unable to get LockedResourceManager") return err } sameResources, leftDifference, _, _ := lockedResourceManager.IsSameResources(lockedResources) //the resource in the leftDifference are not necessarily to be deleted, we need to check if the resource has simply been updated maintinign the sam type/namespace/value. toBeDeleted := getToBeDeletdResources(lockedResources, leftDifference) samePatches, _, _, _ := lockedResourceManager.IsSamePatches(lockedPatches) if !sameResources || !samePatches { err = er.DeleteUnstructuredResources(context, lockedresource.AsListOfUnstructured(toBeDeleted)) if err != nil { er.log.Error(err, "unable to delete unmanaged", "resources", leftDifference) return err } err := lockedResourceManager.Restart(context, lockedResources, lockedPatches, false, config) if err != nil { er.log.Error(err, "unable to restart", "manager", lockedResourceManager) return err } } return nil } func getToBeDeletdResources(neededResources []lockedresource.LockedResource, modifiedResources []lockedresource.LockedResource) []lockedresource.LockedResource { neededResourceSet := strset.New() modifiedResourcesSet := strset.New() modifiedResourceMap := map[string]lockedresource.LockedResource{} toBeDeleted := []lockedresource.LockedResource{} for _, lockerResource := range neededResources { neededResourceSet.Add(apis.GetKeyLong(&lockerResource)) } for _, lockerResource := range modifiedResources { modifiedResourcesSet.Add(apis.GetKeyLong(&lockerResource)) modifiedResourceMap[apis.GetKeyLong(&lockerResource)] = lockerResource } toBeDeletedKeys := strset.Difference(modifiedResourcesSet, neededResourceSet).List() for _, resourceKey := range toBeDeletedKeys { toBeDeleted = append(toBeDeleted, modifiedResourceMap[resourceKey]) } return toBeDeleted } // ManageError manage error sets an error status in the CR and fires an event, finally it returns the error so the operator can re-attempt func (er *EnforcingReconciler) ManageError(context context.Context, instance client.Object, issue error) (reconcile.Result, error) { er.GetRecorder().Event(instance, "Warning", "ProcessingError", issue.Error()) if enforcingReconcileStatusAware, updateStatus := (instance).(v1alpha1.EnforcingReconcileStatusAware); updateStatus { condition := metav1.Condition{ Type: apis.ReconcileError, LastTransitionTime: metav1.Now(), Message: issue.Error(), ObservedGeneration: instance.GetGeneration(), Reason: apis.ReconcileErrorReason, Status: metav1.ConditionTrue, } status := v1alpha1.EnforcingReconcileStatus{ Conditions: apis.AddOrReplaceCondition(condition, enforcingReconcileStatusAware.GetEnforcingReconcileStatus().Conditions), LockedResourceStatuses: er.GetLockedResourceStatuses(instance), LockedPatchStatuses: er.GetLockedPatchStatuses(instance), } enforcingReconcileStatusAware.SetEnforcingReconcileStatus(status) err := er.GetClient().Status().Update(context, instance) if err != nil { if errors.IsResourceExpired(err) { er.log.Info("unable to update status for", "object version", instance.GetResourceVersion(), "resource version expired, will trigger another reconcile cycle", "") } else { er.log.Error(err, "unable to update status for", "object", instance) } return reconcile.Result{}, err } } else { er.log.V(1).Info("object is not ReconcileStatusAware, not setting status") } return reconcile.Result{}, issue } // ManageSuccess will update the status of the CR and return a successful reconcile result func (er *EnforcingReconciler) ManageSuccess(context context.Context, instance client.Object) (reconcile.Result, error) { if enforcingReconcileStatusAware, updateStatus := (instance).(v1alpha1.EnforcingReconcileStatusAware); updateStatus { condition := metav1.Condition{ Type: apis.ReconcileSuccess, LastTransitionTime: metav1.Now(), ObservedGeneration: instance.GetGeneration(), Reason: apis.ReconcileSuccessReason, Status: metav1.ConditionTrue, } status := v1alpha1.EnforcingReconcileStatus{ Conditions: apis.AddOrReplaceCondition(condition, enforcingReconcileStatusAware.GetEnforcingReconcileStatus().Conditions), LockedResourceStatuses: er.GetLockedResourceStatuses(instance), LockedPatchStatuses: er.GetLockedPatchStatuses(instance), } enforcingReconcileStatusAware.SetEnforcingReconcileStatus(status) err := er.GetClient().Status().Update(context, instance) if err != nil { if errors.IsResourceExpired(err) { er.log.Info("unable to update status for", "object version", instance.GetResourceVersion(), "resource version expired, will trigger another reconcile cycle", "") } else { er.log.Error(err, "unable to update status for", "object", instance) } return reconcile.Result{}, err } } else { er.log.V(1).Info("object is not ReconcileStatusAware, not setting status") } return reconcile.Result{}, nil } // GetLockedResourceStatuses returns the status for all LockedResources func (er *EnforcingReconciler) GetLockedResourceStatuses(instance client.Object) map[string]v1alpha1.Conditions { lockedResourceManager, err := er.getLockedResourceManager(instance) if err != nil { er.log.Error(err, "unable to get locked resource manager for", "parent", instance) return map[string]v1alpha1.Conditions{} } lockedResourceReconcileStatuses := map[string]v1alpha1.Conditions{} for _, lockedResourceReconciler := range lockedResourceManager.GetResourceReconcilers() { status := lockedResourceReconciler.GetStatus() if er.returnOnlyFailingStatuses { if lastCondition, ok := apis.GetLastCondition(status); ok && apis.IsErrorCondition(lastCondition) { lockedResourceReconcileStatuses[apis.GetKeyLong(&lockedResourceReconciler.Resource)] = status } } else { lockedResourceReconcileStatuses[apis.GetKeyLong(&lockedResourceReconciler.Resource)] = status } } return lockedResourceReconcileStatuses } // GetLockedPatchStatuses returns the status for all LockedPatches func (er *EnforcingReconciler) GetLockedPatchStatuses(instance client.Object) map[string]v1alpha1.ConditionMap { lockedResourceManager, err := er.getLockedResourceManager(instance) if err != nil { er.log.Error(err, "unable to get locked resource manager for", "parent", instance) return nil } lockedPatchReconcileStatuses := map[string]v1alpha1.ConditionMap{} for _, lockedPatchReconciler := range lockedResourceManager.GetPatchReconcilers() { status := lockedPatchReconciler.GetStatus() for key, conditions := range status { if _, ok := lockedPatchReconcileStatuses[lockedPatchReconciler.GetKey()]; !ok { lockedPatchReconcileStatuses[lockedPatchReconciler.GetKey()] = map[string]v1alpha1.Conditions{} } if er.returnOnlyFailingStatuses { if lastCondition, ok := apis.GetLastCondition(status[key]); ok && apis.IsErrorCondition(lastCondition) { lockedPatchReconcileStatuses[lockedPatchReconciler.GetKey()][key] = conditions } } else { lockedPatchReconcileStatuses[lockedPatchReconciler.GetKey()][key] = conditions } } } return lockedPatchReconcileStatuses } // Terminate will stop the execution for the current instance. It will also optionally delete the locked resources. func (er *EnforcingReconciler) Terminate(instance client.Object, deleteResources bool) error { defer er.removeLockedResourceManager(instance) lockedResourceManager, err := er.getLockedResourceManager(instance) if err != nil { er.log.Error(err, "unable to get locked resource manager for", "parent", instance) return err } if lockedResourceManager.IsStarted() { err = lockedResourceManager.Stop(deleteResources) if err != nil { er.log.Error(err, "unable to stop ", "lockedResourceManager", lockedResourceManager) return err } } return nil } ================================================ FILE: pkg/util/lockedresourcecontroller/locked-resource-manager.go ================================================ package lockedresourcecontroller import ( "context" "encoding/json" "errors" "github.com/go-logr/logr" multierror "github.com/hashicorp/go-multierror" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/apis" "github.com/redhat-cop/operator-utils/pkg/util/discoveryclient" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedpatch" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource/lockedresourceset" "github.com/redhat-cop/operator-utils/pkg/util/stoppablemanager" "github.com/redhat-cop/operator-utils/pkg/util/templates" "github.com/scylladb/go-set/strset" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/validation" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" ) // LockedResourceManager is a manager designed to manage a set of LockedResourceReconciler. // Each reconciler can handle a LockedResource. // LockedResourceManager is designed to be sued within an operator to enforce a set of resources. // It has methods to start and stop the enforcing and to detect whether a set of resources is equal to the currently enforce set. type LockedResourceManager struct { stoppableManager *stoppablemanager.StoppableManager resources []lockedresource.LockedResource resourceReconcilers []*LockedResourceReconciler patches []lockedpatch.LockedPatch patchReconcilers []*LockedPatchReconciler config *rest.Config options manager.Options parent client.Object statusChange chan<- event.GenericEvent clusterWatchers bool log logr.Logger } // NewLockedResourceManager build a new LockedResourceManager // config: the rest config client to be used by the controllers // options: the manager options // parent: an object to which send notification when a recocilianton cicle completes for one of the reconcilers // statusChange: a channel through which send the notifications func NewLockedResourceManager(config *rest.Config, options manager.Options, parent client.Object, statusChange chan<- event.GenericEvent, clusterWatchers bool) (LockedResourceManager, error) { lockedResourceManager := LockedResourceManager{ config: config, options: options, parent: parent, statusChange: statusChange, clusterWatchers: clusterWatchers, log: ctrl.Log.WithName("locker-resource-manager").WithName(apis.GetKeyShort(parent)), } return lockedResourceManager, nil } // GetResources returns the currently enforced resources func (lrm *LockedResourceManager) GetResources() []lockedresource.LockedResource { return lrm.resources } // GetPatches returns the currently enforced patches func (lrm *LockedResourceManager) GetPatches() []lockedpatch.LockedPatch { return lrm.patches } // SetResources set the resources to be enforced. Can be called only when the LockedResourceManager is stopped. func (lrm *LockedResourceManager) SetResources(resources []lockedresource.LockedResource) error { if lrm.stoppableManager != nil && lrm.stoppableManager.IsStarted() { return errors.New("cannot set resources while enforcing is on") } err := lrm.validateLockedResources(resources) if err != nil { lrm.log.Error(err, "unable to validate resources against running api server") return err } lrm.resources = resources return nil } // SetPatches set the patches to be enforced. Can be called only when the LockedResourceManager is stopped. func (lrm *LockedResourceManager) SetPatches(patches []lockedpatch.LockedPatch) error { if lrm.stoppableManager != nil && lrm.stoppableManager.IsStarted() { return errors.New("cannot set resources while enforcing is on") } // verifyPatchID Uniqueness lockedPatchMap := map[string]lockedpatch.LockedPatch{} for _, lockedPatch := range patches { if lockedPatch.Name == "" { return errors.New("lockedPatch.ID must be initialized") } if _, ok := lockedPatchMap[lockedPatch.Name]; ok { return errors.New("Duplicate patch id: " + lockedPatch.Name) } lockedPatchMap[lockedPatch.Name] = lockedPatch } err := lrm.validateLockedPatches(patches) if err != nil { lrm.log.Error(err, "unable to validate patches against running api server") return err } lrm.patches = patches return nil } // IsStarted returns whether the LockedResourceManager is started func (lrm *LockedResourceManager) IsStarted() bool { return lrm.stoppableManager != nil && lrm.stoppableManager.IsStarted() } // Start starts the LockedResourceManager func (lrm *LockedResourceManager) Start(ctx context.Context, config *rest.Config) error { if lrm.stoppableManager != nil && lrm.stoppableManager.IsStarted() { return nil } //diabling metrics options := lrm.options options.MetricsBindAddress = "0" options.LeaderElection = false if !lrm.clusterWatchers { namespaces := lrm.scanNamespaces() lrm.log.V(1).Info("starting multicache with the following ", "namespaces", namespaces) options.NewCache = cache.MultiNamespacedCacheBuilder(namespaces) } stoppableManager, err := stoppablemanager.NewStoppableManager(config, options) lrm.stoppableManager = &stoppableManager if err != nil { lrm.log.Error(err, "unable to create stoppable manager") return err } resourceReconcilers := []*LockedResourceReconciler{} for _, resource := range lrm.resources { reconciler, err := NewLockedObjectReconciler(lrm.stoppableManager.Manager, resource.Unstructured, resource.ExcludedPaths, lrm.statusChange, lrm.parent) if err != nil { lrm.log.Error(err, "unable to create reconciler", "for locked resource", resource) return err } resourceReconcilers = append(resourceReconcilers, reconciler) } lrm.resourceReconcilers = resourceReconcilers patchReconcilers := []*LockedPatchReconciler{} for _, patch := range lrm.patches { reconciler, err := NewLockedPatchReconciler(lrm.stoppableManager.Manager, patch, lrm.statusChange, lrm.parent) if err != nil { lrm.log.Error(err, "unable to create reconciler", "for locked patch", patch) return err } patchReconcilers = append(patchReconcilers, reconciler) } lrm.patchReconcilers = patchReconcilers lrm.stoppableManager.Start(ctx) return nil } // Stop stops the LockedResourceManager. // deleteResource controls whether the managed resources should be deleted or left in place // notice that lrm will always succeed at stopping the manager, but it might fail at deleting resources func (lrm *LockedResourceManager) Stop(deleteResources bool) error { lrm.stoppableManager.Stop() if deleteResources { err := lrm.deleteResources(context.TODO()) if err != nil { lrm.log.Error(err, "unable to delete resources") return err } } return nil } func (lrm *LockedResourceManager) scanNamespaces() []string { namespaceSet := strset.New() for _, resource := range lrm.GetResources() { if resource.GetNamespace() != "" { namespaceSet.Add(resource.GetNamespace()) } } for _, patch := range lrm.GetPatches() { if patch.TargetObjectRef.Namespace != "" { namespaceSet.Add(patch.TargetObjectRef.Namespace) } for _, sourceObj := range patch.SourceObjectRefs { if sourceObj.Namespace != "" { namespaceSet.Add(sourceObj.Namespace) } } } //in case no namesopace is added it means that all of the objects are cluster scoped, then we need to add an emptu string to activate the cache. if len(lrm.GetResources())+len(lrm.GetPatches()) > 0 && len(namespaceSet.List()) == 0 { return []string{""} } return namespaceSet.List() } // Restart restarts the manager with a different set of resources // if deleteResources is set, resources that were enforced are deleted. func (lrm *LockedResourceManager) Restart(ctx context.Context, resources []lockedresource.LockedResource, patches []lockedpatch.LockedPatch, deleteResources bool, config *rest.Config) error { if lrm.IsStarted() { err := lrm.Stop(deleteResources) if err != nil { lrm.log.Error(err, "unable to stop", "deleteResources", deleteResources) return err } } err := lrm.SetResources(resources) if err != nil { lrm.log.Error(err, "unable to set", "resources", resources) return err } err = lrm.SetPatches(patches) if err != nil { lrm.log.Error(err, "unable to set", "patches", patches) return err } return lrm.Start(ctx, config) } // IsSameResources checks whether the currently enforced resources are the same as the ones passed as parameters // same is true is current resources are the same as the resources passed as a parameter // leftDifference contains the resources that are in the current resources but not in passed in the parameter // intersection contains resources that are both in the current resources and the parameter // rightDifference contains the resources that are in the parameter but not in the current resources func (lrm *LockedResourceManager) IsSameResources(resources []lockedresource.LockedResource) (same bool, leftDifference []lockedresource.LockedResource, intersection []lockedresource.LockedResource, rightDifference []lockedresource.LockedResource) { currentResources := lockedresourceset.New(lrm.GetResources()...) newResources := lockedresourceset.New(resources...) leftDifference = lockedresourceset.Difference(currentResources, newResources).List() intersection = lockedresourceset.Intersection(currentResources, newResources).List() rightDifference = lockedresourceset.Difference(newResources, currentResources).List() same = currentResources.IsEqual(newResources) return same, leftDifference, intersection, rightDifference } // IsSamePatches checks whether the currently enforced patches are the same as the ones passed as parameters // same is true is current patches are the same as the patches passed as a parameter // leftDifference contains the patches that are in the current patches but not in passed in the parameter // intersection contains patches that are both in the current patches and the parameter, the patch definition may not be the same, the definitions of those in the parameter are returned // rightDifference contains the patches that are in the parameter but not in the current patches func (lrm *LockedResourceManager) IsSamePatches(patches []lockedpatch.LockedPatch) (same bool, leftDifference []lockedpatch.LockedPatch, intersection []lockedpatch.LockedPatch, rightDifference []lockedpatch.LockedPatch) { currentPatchMap, currentPatches := lockedpatch.GetLockedPatchMap(lrm.GetPatches()) newPatchMap, newPatches := lockedpatch.GetLockedPatchMap(patches) currentPatchSet := strset.New(currentPatches...) newPatchSet := strset.New(newPatches...) leftDifference = lockedpatch.GetLockedPatchesFromLockedPatcheSet(strset.Difference(currentPatchSet, newPatchSet), currentPatchMap) intersection = lockedpatch.GetLockedPatchesFromLockedPatcheSet(strset.Intersection(currentPatchSet, newPatchSet), newPatchMap) rightDifference = lockedpatch.GetLockedPatchesFromLockedPatcheSet(strset.Difference(newPatchSet, currentPatchSet), newPatchMap) same = currentPatchSet.IsEqual(newPatchSet) //we also need to check intersection to see if there are differences in the pacth definition for _, patchID := range strset.Intersection(currentPatchSet, newPatchSet).List() { currentPatch, err := json.Marshal(currentPatchMap[patchID]) if err != nil { lrm.log.Error(err, "unable to Marshall", "currentPatch", currentPatchMap[patchID]) return false, leftDifference, intersection, rightDifference } newPatch, err := json.Marshal(newPatchMap[patchID]) if err != nil { lrm.log.Error(err, "unable to Marshall", "newPatch", newPatchMap[patchID]) return false, leftDifference, intersection, rightDifference } if string(currentPatch) != string(newPatch) { same = false break } } return same, leftDifference, intersection, rightDifference } func (lrm *LockedResourceManager) deleteResources(context context.Context) error { reconcilerBase := util.NewFromManager(lrm.stoppableManager.Manager, lrm.stoppableManager.GetEventRecorderFor("resource-deleter")) for _, resource := range lrm.GetResources() { gvk := resource.Unstructured.GetObjectKind().GroupVersionKind() groupVersion := schema.GroupVersion{Group: gvk.Group, Version: gvk.Version} lrm.stoppableManager.GetScheme().AddKnownTypes(groupVersion, &resource.Unstructured) err := reconcilerBase.DeleteResourceIfExists(context, &resource.Unstructured) if err != nil { lrm.log.Error(err, "unable to delete", "resource", resource.Unstructured) return err } } return nil } // GetResourceReconcilers return the currently active resource reconcilers func (lrm *LockedResourceManager) GetResourceReconcilers() []*LockedResourceReconciler { if lrm.IsStarted() { return lrm.resourceReconcilers } return []*LockedResourceReconciler{} } func (lrm *LockedResourceManager) validateLockedResources(lockedResources []lockedresource.LockedResource) error { ctx := context.TODO() ctx = context.WithValue(ctx, "restConfig", lrm.config) ctx = log.IntoContext(ctx, lrm.log) discoveryClient, err := discovery.NewDiscoveryClientForConfig(lrm.config) if err != nil { lrm.log.Error(err, "unable to create discovery client") return err } //lockedResourceAPIResource. // validate the unstructured object is conformant to the openapi doc, err := discoveryClient.OpenAPISchema() if err != nil { lrm.log.Error(err, "unable to get openapi schema") return err } resources, err := openapi.NewOpenAPIData(doc) if err != nil { lrm.log.Error(err, "unable to get resources from openapi doc") return err } schemaValidation := validation.NewSchemaValidation(resources) result := &multierror.Error{} for _, lockedResource := range lockedResources { defined, err := discoveryclient.IsUnstructuredDefined(ctx, &lockedResource.Unstructured) if err != nil { lrm.log.Error(err, "unable to validate", "unstructured", lockedResource.Unstructured) result = multierror.Append(result, err) continue } if !defined { result = multierror.Append(result, errors.New("resource type:"+lockedResource.Unstructured.GroupVersionKind().String()+"not defined")) continue } err = templates.ValidateUnstructured(ctx, &lockedResource.Unstructured, schemaValidation) if err != nil { lrm.log.Error(err, "unable to validate", "unstructured", lockedResource.Unstructured) result = multierror.Append(result, err) continue } namespaced, err := discoveryclient.IsUnstructuredNamespaced(ctx, &lockedResource.Unstructured) if err != nil { lrm.log.Error(err, "unable to determine if namespaced", "unstructured", lockedResource.Unstructured) result = multierror.Append(result, err) continue } if namespaced && lockedResource.Unstructured.GetNamespace() == "" { err := errors.New("namespaced resources must specify a namespace") lrm.log.Error(err, "unable to validate", "unstructured", lockedResource.Unstructured) result = multierror.Append(result, err) continue } } if result.ErrorOrNil() != nil { lrm.log.Error(result, "encountered errors during resources validation") return result } return nil } // GetPatchReconcilers return the currently active patch reconcilers func (lrm *LockedResourceManager) GetPatchReconcilers() []*LockedPatchReconciler { if lrm.IsStarted() { return lrm.patchReconcilers } return []*LockedPatchReconciler{} } func (lrm *LockedResourceManager) validateLockedPatches(patches []lockedpatch.LockedPatch) error { ctx := context.TODO() ctx = context.WithValue(ctx, "restConfig", lrm.config) ctx = log.IntoContext(ctx, lrm.log) result := &multierror.Error{} for _, lockedPatch := range patches { GVKs := []schema.GroupVersionKind{} for i := range lockedPatch.SourceObjectRefs { GVKs = append(GVKs, schema.FromAPIVersionAndKind(lockedPatch.SourceObjectRefs[i].APIVersion, lockedPatch.SourceObjectRefs[i].Kind)) } GVKs = append(GVKs, schema.FromAPIVersionAndKind(lockedPatch.TargetObjectRef.APIVersion, lockedPatch.TargetObjectRef.Kind)) for i := range GVKs { defined, err := discoveryclient.IsGVKDefined(ctx, GVKs[i]) if err != nil { lrm.log.Error(err, "undefined resource in this cluster", "gvk", GVKs[i]) result = multierror.Append(result, err) continue } if !defined { result = multierror.Append(result, errors.New("resource type:"+GVKs[i].String()+"not defined")) continue } // if resource.Namespaced && objref.Namespace == "" { // err := errors.New("namespace must be specified for namespaced resources") // lrm.log.Error(err, "unable to validate", "objectref", objref) // result = multierror.Append(result, err) // continue // } } } if result.ErrorOrNil() != nil { lrm.log.Error(result, "encountered errors during patch validation") return result } return nil } ================================================ FILE: pkg/util/lockedresourcecontroller/lockedpatch/lockedpatch.go ================================================ package lockedpatch import ( "text/template" "github.com/go-logr/logr" utilsapi "github.com/redhat-cop/operator-utils/api/v1alpha1" utilstemplate "github.com/redhat-cop/operator-utils/pkg/util/templates" "github.com/scylladb/go-set/strset" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" ) var log = ctrl.Log.WithName("lockedpatch") // LockedPatch represents a patch that needs to be enforced. type LockedPatch struct { Name string `json:"name,omitempty"` SourceObjectRefs []utilsapi.SourceObjectReference `json:"sourceObjectRefs,omitempty"` TargetObjectRef utilsapi.TargetObjectReference `json:"targetObjectRef,omitempty"` PatchType types.PatchType `json:"patchType,omitempty"` PatchTemplate string `json:"patchTemplate,omitempty"` Template template.Template `json:"-"` } // GetKey returns a not so unique key for a patch func (lp *LockedPatch) GetKey() string { return lp.Name } // GetLockedPatchMap returns a map and a slice of LockedPatch, useful for set based operations. Needed for internal implementation. func GetLockedPatchMap(lockedPatches []LockedPatch) (map[string]LockedPatch, []string) { lockedPatchMap := map[string]LockedPatch{} lockedPatcheIDs := []string{} for _, lockedPatch := range lockedPatches { lockedPatchMap[lockedPatch.Name] = lockedPatch lockedPatcheIDs = append(lockedPatcheIDs, lockedPatch.Name) } return lockedPatchMap, lockedPatcheIDs } func GetLockedPatchesFromLockedPatcheSet(lockedPatchSet *strset.Set, lockedPatchMap map[string]LockedPatch) []LockedPatch { lockedPatches := []LockedPatch{} for _, lockedPatchID := range lockedPatchSet.List() { lockedPatches = append(lockedPatches, lockedPatchMap[lockedPatchID]) } return lockedPatches } // GetLockedPatches returns a slice of LockedPatches from a slice of apis.Patches func GetLockedPatches(patches map[string]utilsapi.PatchSpec, config *rest.Config, logger logr.Logger) ([]LockedPatch, error) { lockedPatches := []LockedPatch{} for key, patch := range patches { template, err := template.New(patch.PatchTemplate).Funcs(utilstemplate.AdvancedTemplateFuncMap(config, logger)).Parse(patch.PatchTemplate) if err != nil { log.Error(err, "unable to parse ", "template", patch.PatchTemplate) return []LockedPatch{}, err } lockedPatches = append(lockedPatches, LockedPatch{ SourceObjectRefs: patch.SourceObjectRefs, PatchTemplate: patch.PatchTemplate, PatchType: patch.PatchType, TargetObjectRef: patch.TargetObjectRef, Template: *template, Name: key, }) } return lockedPatches, nil } ================================================ FILE: pkg/util/lockedresourcecontroller/lockedresource/lockedresource.go ================================================ package lockedresource import ( "context" "encoding/json" "text/template" "github.com/go-logr/logr" utilsapi "github.com/redhat-cop/operator-utils/api/v1alpha1" utilstemplates "github.com/redhat-cop/operator-utils/pkg/util/templates" "github.com/scylladb/go-set/strset" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) var innerlog = ctrl.Log.WithName("lockedresource") // LockedResource represents a resource to be locked down by a LockedResourceReconciler within a LockedResourceManager type LockedResource struct { // unstructured.Unstructured is the resource to be locked unstructured.Unstructured `json:"usntructured,omitempty"` // ExcludedPaths are the jsonPaths to be excluded when consider whether the resource has changed ExcludedPaths []string `json:"excludedPaths,omitempty"` } // AsListOfUnstructured given a list of LockedResource, returns a list of unstructured.Unstructured func AsListOfUnstructured(lockedResources []LockedResource) []unstructured.Unstructured { unstructuredList := []unstructured.Unstructured{} for _, lockedResource := range lockedResources { unstructuredList = append(unstructuredList, lockedResource.Unstructured) } return unstructuredList } // GetKey returns the marshalled resource func (lr *LockedResource) GetKey() string { bb, err := lr.Unstructured.MarshalJSON() if err != nil { innerlog.Error(err, "unable to marshall", "unstructured", lr.Unstructured) panic(err) } return string(bb) } // GetLockedResources turns an array of Resources as read from an API into an array of LockedResources, usable by the LockedResourceManager func GetLockedResources(resources []utilsapi.LockedResource) ([]LockedResource, error) { lockedResources := []LockedResource{} for _, resource := range resources { bb, err := yaml.YAMLToJSON(resource.Object.Raw) if err != nil { innerlog.Error(err, "Error transforming yaml to json", "raw", resource.Object.Raw) return []LockedResource{}, err } obj := &unstructured.Unstructured{} err = json.Unmarshal(bb, obj) if err != nil { innerlog.Error(err, "Error unmarshalling json manifest", "manifest", string(bb)) return []LockedResource{}, err } lockedResources = append(lockedResources, LockedResource{ Unstructured: *obj, ExcludedPaths: resource.ExcludedPaths, }) } return lockedResources, nil } var templates = map[string]*template.Template{} // GetLockedResourcesFromTemplates Keep backwards compatability with existing consumers func GetLockedResourcesFromTemplates(resources []utilsapi.LockedResourceTemplate, params interface{}) ([]LockedResource, error) { return GetLockedResourcesFromTemplatesWithRestConfig(resources, nil, params) } // GetLockedResourcesFromTemplatesWithRestConfig turns an array of ResourceTemplates as read from an API into an array of LockedResources using a params to process the templates func GetLockedResourcesFromTemplatesWithRestConfig(resources []utilsapi.LockedResourceTemplate, config *rest.Config, params interface{}) ([]LockedResource, error) { lockedResources := []LockedResource{} ctx := context.TODO() ctx = context.WithValue(ctx, "restConfig", config) ctx = log.IntoContext(ctx, innerlog) for _, resource := range resources { template, err := getTemplate(&resource, config, innerlog) if err != nil { innerlog.Error(err, "unable to retrieve template for", "resource", resource) return []LockedResource{}, nil } objs, err := utilstemplates.ProcessTemplateArray(ctx, params, template) if err != nil { innerlog.Error(err, "unable to process template for", "resource", resource, "params", params) return []LockedResource{}, nil } for _, obj := range objs { lockedResources = append(lockedResources, LockedResource{ Unstructured: obj, ExcludedPaths: resource.ExcludedPaths, }) } } return lockedResources, nil } func getTemplate(resource *utilsapi.LockedResourceTemplate, config *rest.Config, logger logr.Logger) (*template.Template, error) { tmpl, ok := templates[resource.ObjectTemplate] var err error if !ok { tmpl, err = template.New(resource.ObjectTemplate).Funcs(utilstemplates.AdvancedTemplateFuncMap(config, logger)).Parse(resource.ObjectTemplate) if err != nil { innerlog.Error(err, "unable to parse", "template", resource.ObjectTemplate) return nil, err } templates[resource.ObjectTemplate] = tmpl } return tmpl, nil } // DefaultExcludedPaths represents paths that are exlcuded by default in all resources var DefaultExcludedPaths = []string{".metadata", ".status", ".spec.replicas"} // DefaultExcludedPathsSet represents paths that are exlcuded by default in all resources var DefaultExcludedPathsSet = strset.New(DefaultExcludedPaths...) // GetResources returs an arrays of apis.Resources from an arya of LockedResources, useful for mass operations on the LockedResources func GetResources(lockedResources []LockedResource) []client.Object { resources := []client.Object{} for _, lockedResource := range lockedResources { resources = append(resources, &lockedResource.Unstructured) } return resources } ================================================ FILE: pkg/util/lockedresourcecontroller/lockedresource/lockedresourceset/lockedresourceset.go ================================================ // Copyright (C) 2017 ScyllaDB // Use of this source code is governed by a ALv2-style // license that can be found at https://github.com/scylladb/go-set/LICENSE. package lockedresourceset import ( "fmt" "math" "strings" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" ) var ( // helpful to not write everywhere struct{}{} nonExistent lockedresource.LockedResource ) // Set is the main set structure that holds all the data // and methods used to working with the set. type Set struct { m map[string]lockedresource.LockedResource } // New creates and initializes a new Set. func New(ts ...lockedresource.LockedResource) *Set { s := NewWithSize(len(ts)) s.Add(ts...) return s } // NewWithSize creates a new Set and gives make map a size hint. func NewWithSize(size int) *Set { return &Set{make(map[string]lockedresource.LockedResource, size)} } // Add includes the specified items (one or more) to the Set. The underlying // Set s is modified. If passed nothing it silently returns. func (s *Set) Add(items ...lockedresource.LockedResource) { for _, item := range items { s.m[item.GetKey()] = item } } // Remove deletes the specified items from the Set. The underlying Set s is // modified. If passed nothing it silently returns. func (s *Set) Remove(items ...lockedresource.LockedResource) { for _, item := range items { delete(s.m, item.GetKey()) } } // Pop deletes and returns an item from the Set. The underlying Set s is // modified. If Set is empty, the zero value is returned. func (s *Set) Pop() lockedresource.LockedResource { for item, value := range s.m { delete(s.m, item) return value } return nonExistent } // Pop2 tries to delete and return an item from the Set. The underlying Set s // is modified. The second value is a bool that is true if the item existed in // the set, and false if not. If Set is empty, the zero value and false are // returned. func (s *Set) Pop2() (lockedresource.LockedResource, bool) { for item, value := range s.m { delete(s.m, item) return value, true } return nonExistent, false } // Has looks for the existence of items passed. It returns false if nothing is // passed. For multiple items it returns true only if all of the items exist. func (s *Set) Has(items ...lockedresource.LockedResource) bool { has := false for _, item := range items { if _, has = s.m[item.GetKey()]; !has { break } } return has } // HasAny looks for the existence of any of the items passed. // It returns false if nothing is passed. // For multiple items it returns true if any of the items exist. func (s *Set) HasAny(items ...lockedresource.LockedResource) bool { has := false for _, item := range items { if _, has = s.m[item.GetKey()]; has { break } } return has } // Size returns the number of items in a Set. func (s *Set) Size() int { return len(s.m) } // Clear removes all items from the Set. func (s *Set) Clear() { s.m = make(map[string]lockedresource.LockedResource) } // IsEmpty reports whether the Set is empty. func (s *Set) IsEmpty() bool { return s.Size() == 0 } // IsEqual test whether s and t are the same in size and have the same items. func (s *Set) IsEqual(t *Set) bool { // return false if they are no the same size if s.Size() != t.Size() { return false } equal := true t.Each(func(item lockedresource.LockedResource) bool { _, equal = s.m[item.GetKey()] return equal // if false, Each() will end }) return equal } // IsSubset tests whether t is a subset of s. func (s *Set) IsSubset(t *Set) bool { if s.Size() < t.Size() { return false } subset := true t.Each(func(item lockedresource.LockedResource) bool { _, subset = s.m[item.GetKey()] return subset }) return subset } // IsSuperset tests whether t is a superset of s. func (s *Set) IsSuperset(t *Set) bool { return t.IsSubset(s) } // Each traverses the items in the Set, calling the provided function for each // Set member. Traversal will continue until all items in the Set have been // visited, or if the closure returns false. func (s *Set) Each(f func(item lockedresource.LockedResource) bool) { for _, value := range s.m { if !f(value) { break } } } // Copy returns a new Set with a copy of s. func (s *Set) Copy() *Set { u := NewWithSize(s.Size()) for item, value := range s.m { u.m[item] = value } return u } // String returns a string representation of s func (s *Set) String() string { v := make([]string, 0, s.Size()) for item := range s.m { v = append(v, fmt.Sprintf("%v", item)) } return fmt.Sprintf("[%s]", strings.Join(v, ", ")) } // List returns a slice of all items. There is also StringSlice() and // IntSlice() methods for returning slices of type string or int. func (s *Set) List() []lockedresource.LockedResource { v := make([]lockedresource.LockedResource, 0, s.Size()) for _, value := range s.m { v = append(v, value) } return v } // Merge is like Union, however it modifies the current Set it's applied on // with the given t Set. func (s *Set) Merge(t *Set) { for item, value := range t.m { s.m[item] = value } } // Separate removes the Set items containing in t from Set s. Please aware that // it's not the opposite of Merge. func (s *Set) Separate(t *Set) { for item := range t.m { delete(s.m, item) } } // Union is the merger of multiple sets. It returns a new set with all the // elements present in all the sets that are passed. func Union(sets ...*Set) *Set { maxPos := -1 maxSize := 0 for i, set := range sets { if l := set.Size(); l > maxSize { maxSize = l maxPos = i } } if maxSize == 0 { return New() } u := sets[maxPos].Copy() for i, set := range sets { if i == maxPos { continue } for item, value := range set.m { u.m[item] = value } } return u } // Difference returns a new set which contains items which are in in the first // set but not in the others. func Difference(set1 *Set, sets ...*Set) *Set { s := set1.Copy() for _, set := range sets { s.Separate(set) } return s } // Intersection returns a new set which contains items that only exist in all // given sets. func Intersection(sets ...*Set) *Set { minPos := -1 minSize := math.MaxInt64 for i, set := range sets { if l := set.Size(); l < minSize { minSize = l minPos = i } } if minSize == math.MaxInt64 || minSize == 0 { return New() } t := sets[minPos].Copy() for i, set := range sets { if i == minPos { continue } for item := range t.m { if _, has := set.m[item]; !has { delete(t.m, item) } } } return t } // SymmetricDifference returns a new set which s is the difference of items // which are in one of either, but not in both. func SymmetricDifference(s *Set, t *Set) *Set { u := Difference(s, t) v := Difference(t, s) return Union(u, v) } ================================================ FILE: pkg/util/lockedresourcecontroller/lockedresource/lockedresourceset/lockedresourceset_bench_test.go ================================================ // Copyright (C) 2017 ScyllaDB // Use of this source code is governed by a ALv2-style // license that can be found at https://github.com/scylladb/go-set/LICENSE. package lockedresourceset // import ( // "testing" // "github.com/fatih/set" // "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" // ) // func BenchmarkTypeSafeSetHasNonExisting(b *testing.B) { // b.StopTimer() // var e1 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // b.StartTimer() // s := New() // for i := 0; i < b.N; i++ { // s.Has(e1) // } // } // func BenchmarkInterfaceSetHasNonExisting(b *testing.B) { // b.StopTimer() // var e1 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // b.StartTimer() // s := set.New(set.NonThreadSafe) // for i := 0; i < b.N; i++ { // s.Has(e1) // } // } // func BenchmarkTypeSafeSetHasExisting(b *testing.B) { // b.StopTimer() // var e1 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // b.StartTimer() // s := New() // s.Add(e1) // for i := 0; i < b.N; i++ { // s.Has(e1) // } // } // func BenchmarkInterfaceSetHasExisting(b *testing.B) { // b.StopTimer() // var e1 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // b.StartTimer() // s := set.New(set.NonThreadSafe) // s.Add(e1) // for i := 0; i < b.N; i++ { // s.Has(e1) // } // } // func BenchmarkTypeSafeSetHasExistingMany(b *testing.B) { // s := New() // b.StopTimer() // var e1 lockedresource.LockedResource // for i := 0; i < 10000; i++ { // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // s.Add(v) // if i == 5000 { // e1 = v // } // } // } // b.StartTimer() // for i := 0; i < b.N; i++ { // s.Has(e1) // } // } // func BenchmarkInterfaceSetHasExistingMany(b *testing.B) { // s := set.New(set.NonThreadSafe) // b.StopTimer() // var e1 lockedresource.LockedResource // for i := 0; i < 10000; i++ { // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // s.Add(v) // if i == 5000 { // e1 = v // } // } // } // b.StartTimer() // for i := 0; i < b.N; i++ { // s.Has(e1) // } // } // func BenchmarkTypeSafeSetAdd(b *testing.B) { // b.StopTimer() // var e lockedresource.LockedResource // s := New() // objs := make([]lockedresource.LockedResource, 0, b.N) // for i := 0; i < b.N; i++ { // e := createRandomObject(e) // if v, ok := e.(lockedresource.LockedResource); ok { // objs = append(objs, v) // } // } // b.StartTimer() // for i := 0; i < b.N; i++ { // s.Add(objs[i]) // } // } // func BenchmarkInterfaceSetAdd(b *testing.B) { // b.StopTimer() // var e lockedresource.LockedResource // s := set.New(set.NonThreadSafe) // objs := make([]lockedresource.LockedResource, 0, b.N) // for i := 0; i < b.N; i++ { // e := createRandomObject(e) // if v, ok := e.(lockedresource.LockedResource); ok { // objs = append(objs, v) // } // } // b.StartTimer() // for i := 0; i < b.N; i++ { // s.Add(objs[i]) // } // } ================================================ FILE: pkg/util/lockedresourcecontroller/lockedresource/lockedresourceset/lockedresourceset_test.go ================================================ // Copyright (C) 2017 ScyllaDB // Use of this source code is governed by a ALv2-style // license that can be found at https://github.com/scylladb/go-set/LICENSE. package lockedresourceset // import ( // "fmt" // "math/rand" // "reflect" // "strings" // "testing" // "testing/quick" // "time" // "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" // ) // func TestAdd(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // s.Add(e1) // s.Add(e2) // if len(s.m) != 2 { // t.Errorf("expected 2 entries, got %d", len(s.m)) // } // } // func TestRemove(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // s.Add(e1) // s.Add(e2) // s.Remove(e1) // if len(s.m) != 1 { // t.Errorf("expected 1 entries, got %d", len(s.m)) // } // if _, ok := s.m[e2.GetKey()]; !ok { // t.Errorf("wrong entry %v removed, expected %v", e1, e2) // } // } // func TestPop(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // popped := s.Pop() // if !reflect.DeepEqual(popped, nonExistent) { // t.Errorf("default non existent sentinel not returned, instead got %v", popped) // } // s.Add(e1) // s.Add(e2) // s.Pop() // if len(s.m) != 1 { // t.Errorf("expected 1 entries, got %d", len(s.m)) // } // } // func TestPop2(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // popped, found := s.Pop2() // if !reflect.DeepEqual(popped, nonExistent) { // t.Errorf("default non existent sentinel not returned, instead got %v", popped) // } // if found { // t.Errorf("set is empty, no element should have been found") // } // s.Add(e1) // s.Add(e2) // _, found = s.Pop2() // if len(s.m) != 1 { // t.Errorf("expected 1 entries, got %d", len(s.m)) // } // if !found { // t.Errorf("expected to find an entry") // } // } // func TestHas(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s := New() // if s.Has(e1) { // t.Errorf("expected a new set to not contain %v", e1) // } // s.Add(e1) // s.Add(e2) // if !s.Has(e1) { // t.Errorf("expected the set to contain %v", e1) // } // if !s.Has(e2) { // t.Errorf("expected the set to contain %v", e2) // } // if s.Has(e3) { // t.Errorf("did not expect the set to contain %v", e3) // } // } // func TestHasAny(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s := New() // if s.Has(e1) { // t.Errorf("expected a new set to not contain %v", e1) // } // s.Add(e1) // s.Add(e2) // if !s.HasAny(e1) { // t.Errorf("expected the set to contain %v", e1) // } // if !s.HasAny(e2) { // t.Errorf("expected the set to contain %v", e2) // } // if s.HasAny(e3) { // t.Errorf("did not expect the set to contain %v", e3) // } // if !s.HasAny(e1, e3) { // t.Errorf("expected the set to contain %v", e1) // } // } // func TestSize(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // s.Add(e1) // s.Add(e2) // if s.Size() != 2 { // t.Errorf("expected the set size to be 2 but it was %d", s.Size()) // } // } // func TestClear(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // s.Add(e1) // s.Add(e2) // s.Clear() // if s.Size() != 0 { // t.Errorf("expected the cleared set size to be 0 but it was %d", s.Size()) // } // } // func TestIsEmpty(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // if !s.IsEmpty() { // t.Errorf("expected new set to be empty but it had %d elements", s.Size()) // } // s.Add(e1) // s.Add(e2) // if s.IsEmpty() { // t.Error("expected a set with added items to not be empty") // } // } // func TestIsEqual(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s1 := New() // s2 := New() // if !s1.IsEqual(s2) { // t.Error("expected 2 new sets to be equal") // } // s1.Add(e1) // s1.Add(e2) // if s1.IsEqual(s2) { // t.Errorf("expected 2 different sets to be equal, %v, %v", s1, s2) // } // s2.Add(e1) // s2.Add(e2) // if !s1.IsEqual(s2) { // t.Error("expected 2 sets with the same items added to be equal") // } // } // func TestIsSubset(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s1 := New() // s2 := New() // s1.Add(e1) // s1.Add(e2) // s2.Add(e1) // if !s1.IsSubset(s2) { // t.Errorf("expected %v to be a subset of %v", s2, s1) // } // s2.Add(e2) // s2.Add(e3) // if s1.IsSubset(s2) { // t.Errorf("expected %v not to be a subset of %v", s2, s1) // } // } // func TestIsSuperset(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s1 := New() // s2 := New() // s1.Add(e1) // s1.Add(e2) // s2.Add(e1) // if !s2.IsSuperset(s1) { // t.Errorf("expected %v to be a super set of %v", s1, s2) // } // } // func TestCopy(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s1 := New() // s1.Add(e1) // s1.Add(e2) // s2 := s1.Copy() // if !s2.IsEqual(s1) { // t.Errorf("expected %v to be equal to %v after copy", s1, s2) // } // } // func TestString(t *testing.T) { // var e1 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // s1 := New() // s1.Add(e1) // s := s1.String() // if s == "" { // t.Errorf("expected string representation %v to exist", s) // } // if !strings.HasPrefix(s, "[") && !strings.HasSuffix(s, "]") { // t.Errorf("expected string representation %v to start with '[' and end with ']'", s) // } // } // func TestMerge(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s1 := New() // s2 := New() // s1.Add(e1) // s2.Add(e2) // s1.Merge(s2) // if s1.Size() != 2 { // t.Errorf("expected merged set %v have size 2 but it has %d", s1, s1.Size()) // } // } // func TestList(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s := New() // s.Add(e1) // s.Add(e2) // l := s.List() // if len(l) != s.Size() { // t.Errorf("expected a set of size %d to give a list of size 2 but it has %d", s.Size(), len(l)) // } // for _, e := range l { // if !s.Has(e) { // t.Errorf("listed entry %v not available in the set %s", e, s) // } // } // } // func TestSeparate(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s1 := New() // s2 := New() // s1.Add(e1) // s1.Add(e2) // s2.Add(e2) // s1.Separate(s2) // if s1.Size() != 1 { // t.Errorf("expected separated set %v have size 1 but it has %d", s1, s1.Size()) // } // if s1.Has(e2) { // t.Errorf("separated set %v still contains %v", s1, e2) // } // } // func TestEach(t *testing.T) { // var e1, e2 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // s1 := New() // s1.Add(e1) // s1.Add(e2) // found := make(map[string]bool) // s1.Each(func(item lockedresource.LockedResource) bool { // found[item.GetKey()] = true // return true // }) // if len(found) != 2 { // t.Errorf("not all items traversed only %v", found) // } // found = make(map[string]bool) // count := 0 // s1.Each(func(item lockedresource.LockedResource) bool { // found[item.GetKey()] = true // count++ // if count > 0 { // return false // } // return true // }) // if len(found) != 1 { // t.Errorf("more than expected 1 items traversed %v", found) // } // } // func TestIntersection(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e3) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s1 := New() // s1.Add(e1) // s1.Add(e2) // s2 := New() // s2.Add(e2) // s2.Add(e3) // s3 := Intersection(s1, s2) // if s3.Size() != 1 || !s3.Has(e2) { // t.Errorf("expected the intersection to only contain '%v' but it is %v", e2, s3.List()) // } // } // func TestUnion(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e3) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s1 := New() // s1.Add(e1) // s1.Add(e2) // s2 := New() // s2.Add(e2) // s2.Add(e3) // s3 := Union(s1, s2) // if s3.Size() != 3 || !(s3.Has(e1) && s3.Has(e2) && s3.Has(e3)) { // t.Errorf("expected the intersection to only contain %v but it is %v", e2, s3.List()) // } // } // func TestDifference(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e3) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s1 := New() // s1.Add(e1) // s1.Add(e2) // s2 := New() // s2.Add(e2) // s2.Add(e3) // s3 := Difference(s1, s2) // if s3.Size() != 1 || !s3.Has(e1) { // t.Errorf("expected the intersection to only contain %v but it is %v", e2, s3.List()) // } // } // func TestSymmetricDifference(t *testing.T) { // var e1, e2, e3 lockedresource.LockedResource // e := createRandomObject(e1) // if v, ok := e.(lockedresource.LockedResource); ok { // e1 = v // } // e = createRandomObject(e2) // if v, ok := e.(lockedresource.LockedResource); ok { // e2 = v // } // e = createRandomObject(e3) // if v, ok := e.(lockedresource.LockedResource); ok { // e3 = v // } // s1 := New() // s1.Add(e1) // s1.Add(e2) // s2 := New() // s2.Add(e2) // s2.Add(e3) // s3 := SymmetricDifference(s1, s2) // if s3.Size() != 2 || !(s3.Has(e1) && s3.Has(e3)) { // t.Errorf("expected the intersection to only contain %v but it is %v", e2, s3.List()) // } // } // func createRandomObject(i interface{}) interface{} { // v, ok := quick.Value(reflect.TypeOf(i), rand.New(rand.NewSource(time.Now().UnixNano()))) // if !ok { // panic(fmt.Sprintf("unsupported type %v", i)) // } // return v.Interface() // } ================================================ FILE: pkg/util/lockedresourcecontroller/lockedresource/patch.go ================================================ package lockedresource import ( "encoding/json" "strings" jsonpatch "github.com/evanphx/json-patch" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func FilterOutPaths(obj *unstructured.Unstructured, jsonPaths []string) (*unstructured.Unstructured, error) { doc, err := obj.MarshalJSON() if err != nil { innerlog.Error(err, "unable to marshall", "unstructured", obj) return &unstructured.Unstructured{}, err } patches, err := createPatchesFromJSONPaths(jsonPaths) if err != nil { innerlog.Error(err, "unable to create patches from", "jsonPaths", jsonPaths) return &unstructured.Unstructured{}, err } for _, patch := range patches { decodedPatch, err := jsonpatch.DecodePatch(patch) if err != nil { innerlog.Error(err, "unable to decode", "patch", string(patch)) return &unstructured.Unstructured{}, err } doc1, err := decodedPatch.Apply(doc) if err != nil { if strings.Contains(err.Error(), "Unable to remove nonexistent key") || strings.Contains(err.Error(), "remove operation does not apply: doc is missing path") { continue } innerlog.Error(err, "unable to apply", "patch", patch, "to json", string(doc)) return &unstructured.Unstructured{}, err } doc = doc1 } var result = &unstructured.Unstructured{} err = result.UnmarshalJSON(doc) if err != nil { innerlog.Error(err, "unable to unMarshall", "json", doc) return &unstructured.Unstructured{}, err } return result, nil } // Patch represents a patch operation type Patch struct { Operation string `json:"op"` Path string `json:"path"` } func createPatchesFromJSONPaths(jsonPaths []string) ([][]byte, error) { result := [][]byte{} for _, jsonPath := range jsonPaths { patch := []Patch{ { Operation: "remove", Path: getMergePathFromJSONPath(jsonPath), }, } mpatch, err := json.Marshal(patch) if err != nil { innerlog.Error(err, "unable to marshal", "patch", patch) return [][]byte{}, err } result = append(result, mpatch) } return result, nil } func getMergePathFromJSONPath(jsonPath string) string { //remove "$" if present jsonPath = strings.TrimPrefix(jsonPath, "$") // convert "[" and "]" to "." if strings.HasSuffix(jsonPath, "]") { jsonPath = jsonPath[:len(jsonPath)-2] } jsonPath = strings.ReplaceAll(jsonPath, "[", ".") jsonPath = strings.ReplaceAll(jsonPath, "]", ".") // convert "." to "/" jsonPath = strings.ReplaceAll(jsonPath, ".", "/") return jsonPath } ================================================ FILE: pkg/util/lockedresourcecontroller/patch-reconciler.go ================================================ package lockedresourcecontroller import ( "bytes" "context" "encoding/json" "errors" "reflect" "strings" "sync" "github.com/go-logr/logr" utilsapi "github.com/redhat-cop/operator-utils/api/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/apis" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedpatch" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/client-go/util/jsonpath" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/yaml" ) // LockedPatchReconciler is a reconciler that can enforce a LockedPatch type LockedPatchReconciler struct { util.ReconcilerBase patch lockedpatch.LockedPatch status map[string][]metav1.Condition statusChange chan<- event.GenericEvent parentObject client.Object statusLock sync.Mutex log logr.Logger } // NewLockedPatchReconciler returns a new reconcile.Reconciler func NewLockedPatchReconciler(mgr manager.Manager, patch lockedpatch.LockedPatch, statusChange chan<- event.GenericEvent, parentObject client.Object) (*LockedPatchReconciler, error) { // TODO create the object is it does not exists controllername := "patch-reconciler" reconciler := &LockedPatchReconciler{ log: ctrl.Log.WithName(controllername).WithName(apis.GetKeyShort(parentObject)).WithName(patch.GetKey()), ReconcilerBase: util.NewFromManager(mgr, mgr.GetEventRecorderFor(controllername+"_"+patch.GetKey())), patch: patch, statusChange: statusChange, parentObject: parentObject, statusLock: sync.Mutex{}, status: map[string][]metav1.Condition{ "reconciler": []metav1.Condition([]metav1.Condition{{ Type: "Initializing", LastTransitionTime: metav1.Now(), Status: metav1.ConditionTrue, ObservedGeneration: 0, Reason: "ReconcilerManagerRestarting", }}), }, } controller, err := controller.New(controllername+"_"+patch.GetKey(), mgr, controller.Options{Reconciler: reconciler}) if err != nil { return &LockedPatchReconciler{}, err } //create watcher for target obj := targetObjectRefToRuntimeType(&patch.TargetObjectRef) mgr.GetScheme().AddKnownTypes(schema.FromAPIVersionAndKind(patch.TargetObjectRef.APIVersion, patch.TargetObjectRef.Kind).GroupVersion(), obj) err = controller.Watch(source.Kind(mgr.GetCache(), obj), &handler.EnqueueRequestForObject{}, &targetReferenceModifiedPredicate{ TargetObjectReference: patch.TargetObjectRef, log: reconciler.log.WithName("target-watcher"), restConfig: mgr.GetConfig(), }) if err != nil { return &LockedPatchReconciler{}, err } discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) if err != nil { return &LockedPatchReconciler{}, err } for _, sourceRef := range patch.SourceObjectRefs { obj := sourceObjectRefToRuntimeType(&sourceRef) mgr.GetScheme().AddKnownTypes(schema.FromAPIVersionAndKind(sourceRef.APIVersion, sourceRef.Kind).GroupVersion(), obj) err = controller.Watch(source.Kind(mgr.GetCache(), obj), &enqueueRequestForPatch{ source: &sourceRef, target: &patch.TargetObjectRef, discoveryClient: discoveryClient, restConfig: mgr.GetConfig(), log: reconciler.log.WithName(sourceRef.APIVersion + "/" + sourceRef.Kind + "/" + sourceRef.Namespace + "/" + sourceRef.Name).WithName("source-event-handler"), }, &sourceReferenceModifiedPredicate{ log: reconciler.log.WithName(sourceRef.APIVersion + "/" + sourceRef.Kind + "/" + sourceRef.Namespace + "/" + sourceRef.Name).WithName("source-event-filter"), source: &sourceRef, target: &patch.TargetObjectRef, restConfig: mgr.GetConfig(), }) if err != nil { return &LockedPatchReconciler{}, err } } return reconciler, nil } func sourceObjectRefToRuntimeType(objref *utilsapi.SourceObjectReference) client.Object { obj := &unstructured.Unstructured{} obj.SetKind(objref.Kind) obj.SetAPIVersion(objref.APIVersion) return obj } func targetObjectRefToRuntimeType(objref *utilsapi.TargetObjectReference) client.Object { obj := &unstructured.Unstructured{} obj.SetKind(objref.Kind) obj.SetAPIVersion(objref.APIVersion) return obj } type enqueueRequestForPatch struct { source *utilsapi.SourceObjectReference target *utilsapi.TargetObjectReference discoveryClient *discovery.DiscoveryClient restConfig *rest.Config log logr.Logger } func (e *enqueueRequestForPatch) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { //to see if this event is relevant and we have to do the following: // 1. see if the target is single or multiple // 2. if single just see if it matches, the pass the event. // 3. if multiple see which macth and then pass the event e.log.V(1).Info("enqueue create", "for", evt.Object) ctx = context.WithValue(ctx, "restConfig", e.restConfig) ctx = log.IntoContext(ctx, e.log) multiple, _, err := e.target.IsSelectingMultipleInstances(ctx) if err != nil { e.log.Error(err, "Unable to determine if target resolves to multiple instance", "target", e.target) return } if !multiple { obj, err := e.target.GetReferencedObject(ctx) if err != nil { e.log.Error(err, "Unable to get referenced object", "target", e.target) return } sourceName, sourceNamespace, err := e.source.GetNameAndNamespace(ctx, obj) if err != nil { e.log.Error(err, "Unable to process name and namespace templates", "source", e.source, "param", obj) return } if sourceName == evt.Object.GetName() && sourceNamespace == evt.Object.GetNamespace() { e.log.V(1).Info("enqueing", "request", reconcile.Request{ NamespacedName: types.NamespacedName{ Name: e.target.Name, Namespace: e.target.Namespace, }, }) q.Add(reconcile.Request{ NamespacedName: types.NamespacedName{ Name: e.target.Name, Namespace: e.target.Namespace, }, }) } return } if multiple { objs, err := e.target.GetReferencedObjects(ctx) if err != nil { e.log.Error(err, "Unable to get referenced objects", "target", e.target) return } for i := range objs { sourceName, sourceNamespace, err := e.source.GetNameAndNamespace(ctx, &objs[i]) if err != nil { e.log.Error(err, "Unable to process name and namespace templates", "source", e.source, "param", objs[i]) return } if sourceName == evt.Object.GetName() && sourceNamespace == evt.Object.GetNamespace() { e.log.V(1).Info("enqueing", "request", reconcile.Request{ NamespacedName: types.NamespacedName{ Name: objs[i].GetName(), Namespace: objs[i].GetNamespace(), }, }) q.Add(reconcile.Request{ NamespacedName: types.NamespacedName{ Name: objs[i].GetName(), Namespace: objs[i].GetNamespace(), }, }) } } } } // Update implements EventHandler func (e *enqueueRequestForPatch) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { //to see if this event is relevant and we have to do the following: // 1. see if the target is single or multiple // 2. if single just see if it matches, the pass the event. // 3. if multiple see which macth and then pass the event // TODO this could be optmized to see if the change affected the needed jsonpath e.log.V(1).Info("enqueue update", "for", evt.ObjectNew) ctx = context.WithValue(ctx, "discoveryClient", e.discoveryClient) ctx = context.WithValue(ctx, "restConfig", e.restConfig) ctx = log.IntoContext(ctx, e.log) multiple, _, err := e.target.IsSelectingMultipleInstances(ctx) if err != nil { e.log.Error(err, "Unable to determine if target resolves to multiple instance", "target", e.target) return } if !multiple { obj, err := e.target.GetReferencedObject(ctx) if err != nil { e.log.Error(err, "Unable to get referenced object", "target", e.target) return } sourceName, sourceNamespace, err := e.source.GetNameAndNamespace(ctx, obj) if err != nil { e.log.Error(err, "Unable to process name and namespace templates", "source", e.source, "param", obj) return } if sourceName == evt.ObjectNew.GetName() && sourceNamespace == evt.ObjectNew.GetNamespace() { q.Add(reconcile.Request{ NamespacedName: types.NamespacedName{ Name: e.target.Name, Namespace: e.target.Namespace, }, }) } return } if multiple { objs, err := e.target.GetReferencedObjects(ctx) if err != nil { e.log.Error(err, "Unable to get referenced objects", "target", e.target) return } for i := range objs { sourceName, sourceNamespace, err := e.source.GetNameAndNamespace(ctx, &objs[i]) if err != nil { e.log.Error(err, "Unable to process name and namespace templates", "source", e.source, "param", objs[i]) return } if sourceName == evt.ObjectNew.GetName() && sourceNamespace == evt.ObjectNew.GetNamespace() { q.Add(reconcile.Request{ NamespacedName: types.NamespacedName{ Name: objs[i].GetName(), Namespace: objs[i].GetNamespace(), }, }) } } } } // Delete implements EventHandler func (e *enqueueRequestForPatch) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { } // Generic implements EventHandler func (e *enqueueRequestForPatch) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { } type sourceReferenceModifiedPredicate struct { source *utilsapi.SourceObjectReference target *utilsapi.TargetObjectReference restConfig *rest.Config log logr.Logger } // Update implements default UpdateEvent filter for validating resource version change func (p *sourceReferenceModifiedPredicate) Update(e event.UpdateEvent) bool { //TODO can be optimized by calculating whether we are selecting multiple objects p.log.V(1).Info("filter update", "for", e.ObjectNew) ctx := context.TODO() ctrl.LoggerInto(ctx, p.log) return p.isRelevant(e.ObjectNew) && !compareSourceObjects(ctx, p.source, e.ObjectNew, e.ObjectOld) } func (p *sourceReferenceModifiedPredicate) Create(e event.CreateEvent) bool { //TODO can be optimized by calculating whether we are selecting multiple objects p.log.V(1).Info("filter create", "for", e.Object) return p.isRelevant(e.Object) } func (p *sourceReferenceModifiedPredicate) isRelevant(obj client.Object) bool { // we need to aggressively filter events. // if name and namespaces are not templates, we can check the object if !strings.Contains(p.source.Name, "{{") && !strings.Contains(p.source.Namespace, "{{") { return obj.GetName() == p.source.Name && obj.GetNamespace() == p.source.Namespace } // if target is not selecting multiple instances then we can resolve the templates and test the object ctx := context.TODO() ctrl.LoggerInto(ctx, p.log) ctx = context.WithValue(ctx, "restConfig", p.restConfig) multiple, _, err := p.target.IsSelectingMultipleInstances(ctx) if err != nil { p.log.Error(err, "unable to determine if target object selects multiple instances") return false } if !multiple { tobj, err := p.target.GetReferencedObject(ctx) if err != nil { p.log.Error(err, "unable to get target referenced obect") return false } name, namespace, err := p.source.GetNameAndNamespace(ctx, tobj) if err != nil { p.log.Error(err, "unable to get source name and namespace from target") return false } return name == obj.GetName() && namespace == obj.GetNamespace() } return true } func (p *sourceReferenceModifiedPredicate) Delete(e event.DeleteEvent) bool { // we ignore Delete events because if we loosed references there is no point in trying to recompute the patch return false } func (p *sourceReferenceModifiedPredicate) Generic(e event.GenericEvent) bool { // we ignore Generic events return false } type targetReferenceModifiedPredicate struct { utilsapi.TargetObjectReference restConfig *rest.Config log logr.Logger } // Update implements default UpdateEvent filter for validating resource version change func (p *targetReferenceModifiedPredicate) Update(e event.UpdateEvent) bool { p.log.V(1).Info("filter update", "for", e.ObjectNew) ctx := context.TODO() ctrl.LoggerInto(ctx, p.log) ctx = context.WithValue(ctx, "restConfig", p.restConfig) selected, err := p.TargetObjectReference.Selects(ctx, e.ObjectNew) if err != nil { p.log.Error(err, "unable to determine if current object is selected", "object", e.ObjectNew, "target", p.TargetObjectReference) return false } p.log.V(1).Info("", "selected", selected) if selected { return !compareObjectsWithoutIgnoredFields(e.ObjectNew, e.ObjectOld) } return false } func (p *targetReferenceModifiedPredicate) Create(e event.CreateEvent) bool { p.log.V(1).Info("filter create", "for", e.Object) ctx := context.TODO() ctrl.LoggerInto(ctx, p.log) ctx = context.WithValue(ctx, "restConfig", p.restConfig) selected, err := p.TargetObjectReference.Selects(ctx, e.Object) if err != nil { p.log.Error(err, "unable to determine if current object is selected", "object", e.Object, "target", p.TargetObjectReference) return false } return selected } func (p *targetReferenceModifiedPredicate) Delete(e event.DeleteEvent) bool { // we ignore Delete events because if we loosed references there is no point in trying to recompute the patch return false } func (p *targetReferenceModifiedPredicate) Generic(e event.GenericEvent) bool { // we ignore Generic events return false } // we ignore the fields of resourceVersion and managedFields func compareObjectsWithoutIgnoredFields(changedObjSrc runtime.Object, originalObjSrc runtime.Object) bool { changedObj := changedObjSrc.DeepCopyObject().(*unstructured.Unstructured) originalObj := originalObjSrc.DeepCopyObject().(*unstructured.Unstructured) changedObj.SetManagedFields(nil) changedObj.SetResourceVersion("") originalObj.SetManagedFields(nil) originalObj.SetResourceVersion("") changedObjJSON, _ := json.Marshal(changedObj) originalObjJSON, _ := json.Marshal(originalObj) return (string(changedObjJSON) == string(originalObjJSON)) } func compareSourceObjects(ctx context.Context, sourceObjectReference *utilsapi.SourceObjectReference, changedObjSrc runtime.Object, originalObjSrc runtime.Object) bool { if sourceObjectReference.FieldPath != "" { mlog := log.FromContext(ctx) changedUnstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(changedObjSrc) if err != nil { mlog.Error(err, "unable to convert runtime object to unstructured", "runtime object", changedObjSrc) return false } originalUnstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObjSrc) if err != nil { mlog.Error(err, "unable to convert runtime object to unstructured", "runtime object", originalObjSrc) return false } changedObjSubMap, err := getSubMapFromObject(ctx, &unstructured.Unstructured{Object: changedUnstructuredObj}, sourceObjectReference.FieldPath) if err != nil { mlog.Error(err, "unable to convert get submap from unstructured", "fieldPath", sourceObjectReference.FieldPath, "unstructured", changedUnstructuredObj) return false } originalObjSubMap, err := getSubMapFromObject(ctx, &unstructured.Unstructured{Object: originalUnstructuredObj}, sourceObjectReference.FieldPath) if err != nil { mlog.Error(err, "unable to convert get submap from unstructured", "fieldPath", sourceObjectReference.FieldPath, "unstructured", originalUnstructuredObj) return false } return !reflect.DeepEqual(changedObjSubMap, originalObjSubMap) } else { return compareObjectsWithoutIgnoredFields(changedObjSrc, originalObjSrc) } } // Reconcile method func (lpr *LockedPatchReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { //gather all needed the objects lpr.log.V(1).Info("reconcile", "for", request) ctx = context.WithValue(ctx, "restConfig", lpr.GetRestConfig()) ctx = log.IntoContext(ctx, lpr.log) targetObj, err := lpr.patch.TargetObjectRef.GetReferencedObjectWithName(ctx, request.NamespacedName) if err != nil { lpr.log.Error(err, "unable to retrieve", "target", lpr.patch.TargetObjectRef) return lpr.manageErrorNoTarget(err) } // the first object is always the target object sourceMaps := []interface{}{targetObj.UnstructuredContent()} for i := range lpr.patch.SourceObjectRefs { sourceObj, err := lpr.patch.SourceObjectRefs[i].GetReferencedObject(ctx, targetObj) if err != nil { lpr.log.Error(err, "unable to retrieve", "sourceObjectRef", lpr.patch.SourceObjectRefs[i]) return lpr.manageError(targetObj, err) } sourceMap, err := getSubMapFromObject(ctx, sourceObj, lpr.patch.SourceObjectRefs[i].FieldPath) if err != nil { lpr.log.Error(err, "unable to retrieve", "field", lpr.patch.SourceObjectRefs[i].FieldPath, "from object", sourceObj) return lpr.manageError(targetObj, err) } sourceMaps = append(sourceMaps, sourceMap) } //compute the template var b bytes.Buffer err = lpr.patch.Template.Execute(&b, sourceMaps) if err != nil { lpr.log.Error(err, "unable to process ", "template ", lpr.patch.Template, "parameters", sourceMaps) return lpr.manageError(targetObj, err) } bb, err := yaml.YAMLToJSON(b.Bytes()) if err != nil { lpr.log.Error(err, "unable to convert to json", "processed template", b.String()) return lpr.manageError(targetObj, err) } patch := client.RawPatch(lpr.patch.PatchType, bb) err = lpr.GetClient().Patch(ctx, targetObj, patch) if err != nil { lpr.log.Error(err, "unable to apply ", "patch", patch, "on target", targetObj) return lpr.manageError(targetObj, err) } return lpr.manageSuccess(targetObj) } // GetKey return the patch no so unique identifier func (lpr *LockedPatchReconciler) GetKey() string { return lpr.patch.GetKey() } func getSubMapFromObject(ctx context.Context, obj *unstructured.Unstructured, fieldPath string) (interface{}, error) { mlog := log.FromContext(ctx) if fieldPath == "" { return obj.UnstructuredContent(), nil } jp := jsonpath.New("fieldPath:" + fieldPath) err := jp.Parse("{" + fieldPath + "}") if err != nil { mlog.Error(err, "unable to parse ", "fieldPath", fieldPath) return nil, err } values, err := jp.FindResults(obj.UnstructuredContent()) if err != nil { mlog.Error(err, "unable to apply ", "jsonpath", jp, " to obj ", obj.UnstructuredContent()) return nil, err } if len(values) > 0 && len(values[0]) > 0 { return values[0][0].Interface(), nil } return nil, errors.New("jsonpath returned empty result") } func (lpr *LockedPatchReconciler) manageError(target client.Object, err error) (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileError, LastTransitionTime: metav1.Now(), Message: err.Error(), Reason: apis.ReconcileErrorReason, Status: metav1.ConditionTrue, ObservedGeneration: target.GetGeneration(), } lpr.setStatus(apis.GetKeyShort(target), apis.AddOrReplaceCondition(condition, lpr.GetStatus()[apis.GetKeyShort(target)])) return reconcile.Result{}, err } func (lpr *LockedPatchReconciler) manageErrorNoTarget(err error) (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileError, LastTransitionTime: metav1.Now(), Message: err.Error(), Reason: apis.ReconcileErrorReason, Status: metav1.ConditionTrue, ObservedGeneration: 0, } lpr.setStatus("reconciler", apis.AddOrReplaceCondition(condition, lpr.GetStatus()["reconciler"])) return reconcile.Result{}, err } func (lpr *LockedPatchReconciler) manageSuccess(target client.Object) (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileSuccess, LastTransitionTime: metav1.Now(), Reason: apis.ReconcileSuccessReason, Status: metav1.ConditionTrue, ObservedGeneration: target.GetGeneration(), } lpr.setStatus(apis.GetKeyShort(target), apis.AddOrReplaceCondition(condition, lpr.GetStatus()[apis.GetKeyShort(target)])) return reconcile.Result{}, nil } func (lpr *LockedPatchReconciler) setStatus(key string, conditions []metav1.Condition) { lpr.statusLock.Lock() defer lpr.statusLock.Unlock() lpr.status[key] = conditions if lpr.statusChange != nil { lpr.statusChange <- event.GenericEvent{ Object: lpr.parentObject, } } } // GetStatus returns the status for this reconciler func (lpr *LockedPatchReconciler) GetStatus() map[string][]metav1.Condition { lpr.statusLock.Lock() defer lpr.statusLock.Unlock() return lpr.status } ================================================ FILE: pkg/util/lockedresourcecontroller/resource-reconciler.go ================================================ package lockedresourcecontroller import ( "context" "reflect" "sync" "encoding/json" "github.com/go-logr/logr" "github.com/nsf/jsondiff" "github.com/redhat-cop/operator-utils/pkg/util" "github.com/redhat-cop/operator-utils/pkg/util/apis" "github.com/redhat-cop/operator-utils/pkg/util/dynamicclient" "github.com/redhat-cop/operator-utils/pkg/util/lockedresourcecontroller/lockedresource" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) // LockedResourceReconciler is a reconciler that will lock down a resource to prevent changes from external events. // This reconciler can be configured to ignore a set of json path. Changed occurring on the ignored path will be ignored, and therefore allowed by the reconciler type LockedResourceReconciler struct { Resource unstructured.Unstructured ExcludePaths []string util.ReconcilerBase status []metav1.Condition statusChange chan<- event.GenericEvent statusLock sync.Mutex parentObject client.Object firstReconcile chan event.GenericEvent log logr.Logger } // NewLockedObjectReconciler returns a new reconcile.Reconciler func NewLockedObjectReconciler(mgr manager.Manager, object unstructured.Unstructured, excludePaths []string, statusChange chan<- event.GenericEvent, parentObject client.Object) (*LockedResourceReconciler, error) { controllername := "resource-reconciler" reconciler := &LockedResourceReconciler{ log: ctrl.Log.WithName(controllername).WithName(apis.GetKeyShort(parentObject)).WithName(apis.GetKeyLong(&object)), ReconcilerBase: util.NewFromManager(mgr, mgr.GetEventRecorderFor(controllername+"_"+apis.GetKeyLong(&object))), Resource: object, ExcludePaths: excludePaths, statusChange: statusChange, parentObject: parentObject, statusLock: sync.Mutex{}, firstReconcile: make(chan event.GenericEvent), status: []metav1.Condition([]metav1.Condition{{ Type: "Initializing", LastTransitionTime: metav1.Now(), Status: metav1.ConditionTrue, ObservedGeneration: object.GetGeneration(), Reason: "ReconcilerManagerRestarting", }}), } go func() { reconciler.firstReconcile <- event.GenericEvent{ Object: &object, } }() controller, err := controller.New("controller_locked_object_"+apis.GetKeyLong(&object), mgr, controller.Options{Reconciler: reconciler}) if err != nil { reconciler.log.Error(err, "unable to create new controller", "with reconciler", reconciler) return &LockedResourceReconciler{}, err } gvk := object.GetObjectKind().GroupVersionKind() groupVersion := schema.GroupVersion{Group: gvk.Group, Version: gvk.Version} mgr.GetScheme().AddKnownTypes(groupVersion, &object) err = controller.Watch(source.Kind(mgr.GetCache(), &object), &handler.EnqueueRequestForObject{}, &resourceModifiedPredicate{ name: object.GetName(), namespace: object.GetNamespace(), lrr: reconciler, }) if err != nil { reconciler.log.Error(err, "unable to create new watch", "with source", object) return &LockedResourceReconciler{}, err } err = controller.Watch( &source.Channel{Source: reconciler.firstReconcile}, &handler.EnqueueRequestForObject{}, ) if err != nil { return &LockedResourceReconciler{}, err } return reconciler, nil } // Reconcile contains the reconcile logic for LockedResourceReconciler func (lor *LockedResourceReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { lor.log.Info("reconcile called for", "object", apis.GetKeyLong(&lor.Resource), "request", request) ctx = context.WithValue(ctx, "restConfig", lor.GetRestConfig()) ctx = log.IntoContext(ctx, lor.log) client, err := dynamicclient.GetDynamicClientOnUnstructured(ctx, &lor.Resource) if err != nil { lor.log.Error(err, "unable to get dynamicClient", "on object", lor.Resource) return lor.manageErrorNoInstance(err) } instance, err := client.Get(ctx, lor.Resource.GetName(), v1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { // if not found we have to recreate it. err = lor.CreateOrUpdateResource(ctx, nil, "", lor.Resource.DeepCopy()) if err != nil { lor.log.Error(err, "unable to create or update", "object", lor.Resource) return lor.manageErrorNoInstance(err) } return lor.manageSuccessNoInstance() } // Error reading the object - requeue the request. lor.log.Error(err, "unable to lookup", "object", lor.Resource) return lor.manageError(instance, err) } equal, err := lor.isEqual(instance) if err != nil { lor.log.Error(err, "unable to determine if", "object", lor.Resource, "is equal to object", instance) return lor.manageError(instance, err) } if !equal { lor.log.V(1).Info("determined that resources are NOT equal", "differences", lor.logDiff(instance)) patch, err := lockedresource.FilterOutPaths(&lor.Resource, lor.ExcludePaths) if err != nil { lor.log.Error(err, "unable to filter out ", "excluded paths", lor.ExcludePaths, "from object", lor.Resource) return lor.manageError(instance, err) } if err != nil { lor.log.Error(err, "unable to marshall ", "object", patch) return lor.manageError(instance, err) } patchBytes, err := json.Marshal(patch) if err != nil { lor.log.Error(err, "unable to marshall ", "object", patch) return lor.manageError(instance, err) } _, err = client.Patch(ctx, instance.GetName(), types.MergePatchType, patchBytes, metav1.PatchOptions{}) if err != nil { lor.log.Error(err, "unable to patch ", "object", instance, "with patch", string(patchBytes)) return lor.manageError(instance, err) } return lor.manageSuccess(instance) } lor.log.V(1).Info("determined that resources are equal") return lor.manageSuccess(instance) } func (lor *LockedResourceReconciler) isEqual(instance *unstructured.Unstructured) (bool, error) { left, err := lockedresource.FilterOutPaths(&lor.Resource, lor.ExcludePaths) if err != nil { return false, err } right, err := lockedresource.FilterOutPaths(instance, lor.ExcludePaths) if err != nil { return false, err } return reflect.DeepEqual(left, right), nil } func (lor *LockedResourceReconciler) logDiff(instance *unstructured.Unstructured) string { fi, err := lockedresource.FilterOutPaths(instance, lor.ExcludePaths) if err != nil { return "unable to log differences" } fr, err := lockedresource.FilterOutPaths(&lor.Resource, lor.ExcludePaths) if err != nil { return "unable to log differences" } fib, err := json.Marshal(fi) if err != nil { return "unable to log differences" } frb, err := json.Marshal(fr) if err != nil { return "unable to log differences" } opts := jsondiff.DefaultJSONOptions() opts.SkipMatches = true opts.Indent = "\t" _, diff := jsondiff.Compare(fib, frb, &opts) return diff } type resourceModifiedPredicate struct { name string namespace string lrr *LockedResourceReconciler predicate.Funcs } // Update implements default UpdateEvent filter for validating resource version change func (p *resourceModifiedPredicate) Update(e event.UpdateEvent) bool { if e.ObjectNew.GetNamespace() == p.namespace && e.ObjectNew.GetName() == p.name { return true } return false } func (p *resourceModifiedPredicate) Create(e event.CreateEvent) bool { if e.Object.GetNamespace() == p.namespace && e.Object.GetName() == p.name { return true } return false } func (p *resourceModifiedPredicate) Delete(e event.DeleteEvent) bool { if e.Object.GetNamespace() == p.namespace && e.Object.GetName() == p.name { // we return true only if the enclosing namespace is not also being deleted if e.Object.GetNamespace() != "" { namespace := corev1.Namespace{} // Use non-cached client since client's cache may be namespaced err := p.lrr.GetAPIReader().Get(context.TODO(), types.NamespacedName{Name: e.Object.GetNamespace()}, &namespace) if err != nil { p.lrr.log.Error(err, "unable to retrieve ", "namespace", "e.Meta.GetNamespace()") // If the request failed return "true" as the k8s API will deny any create/update operation in a // Namespace that's marked for termination. Returning false here causes resources not being reconciled // in namespaced installations (Namespace requires a client with cluster scoped permissions) return true } if util.IsBeingDeleted(&namespace) { return false } } return true } return false } func (lor *LockedResourceReconciler) manageError(instance *unstructured.Unstructured, err error) (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileError, LastTransitionTime: metav1.Now(), Message: err.Error(), Reason: apis.ReconcileErrorReason, Status: metav1.ConditionTrue, ObservedGeneration: func() int64 { if instance != nil { return instance.GetGeneration() } else { return 0 } }(), } lor.setStatus(apis.AddOrReplaceCondition(condition, lor.GetStatus())) return reconcile.Result{}, err } func (lor *LockedResourceReconciler) manageErrorNoInstance(err error) (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileError, LastTransitionTime: metav1.Now(), Message: err.Error(), Reason: apis.ReconcileErrorReason, Status: metav1.ConditionTrue, ObservedGeneration: 0, } lor.setStatus(apis.AddOrReplaceCondition(condition, lor.GetStatus())) return reconcile.Result{}, err } func (lor *LockedResourceReconciler) manageSuccess(instance *unstructured.Unstructured) (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileSuccess, LastTransitionTime: metav1.Now(), Reason: apis.ReconcileSuccessReason, Status: metav1.ConditionTrue, ObservedGeneration: instance.GetGeneration(), } lor.setStatus(apis.AddOrReplaceCondition(condition, lor.GetStatus())) return reconcile.Result{}, nil } func (lor *LockedResourceReconciler) manageSuccessNoInstance() (reconcile.Result, error) { condition := metav1.Condition{ Type: apis.ReconcileSuccess, LastTransitionTime: metav1.Now(), Reason: apis.ReconcileSuccessReason, Status: metav1.ConditionTrue, ObservedGeneration: 0, } lor.setStatus(apis.AddOrReplaceCondition(condition, lor.GetStatus())) return reconcile.Result{}, nil } func (lor *LockedResourceReconciler) setStatus(status []metav1.Condition) { lor.statusLock.Lock() defer lor.statusLock.Unlock() lor.status = status if lor.statusChange != nil { lor.statusChange <- event.GenericEvent{ Object: lor.parentObject, } } } // GetStatus returns the latest reconcile status func (lor *LockedResourceReconciler) GetStatus() []metav1.Condition { lor.statusLock.Lock() defer lor.statusLock.Unlock() status := lor.status return status } ================================================ FILE: pkg/util/owner.go ================================================ /* Copyright 2019 Red Hat, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) func IsOwner(owner, owned metav1.Object) bool { runtimeObj, ok := (owner).(runtime.Object) if !ok { return false } for _, ownerRef := range owned.GetOwnerReferences() { if ownerRef.Name == owner.GetName() && ownerRef.UID == owner.GetUID() && ownerRef.Kind == runtimeObj.GetObjectKind().GroupVersionKind().Kind { return true } } return false } ================================================ FILE: pkg/util/predicates.go ================================================ /* Copyright 2019 Red Hat, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "reflect" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" ) // ResourceGenerationOrFinalizerChangedPredicate this predicate will fire an update event when the spec of a resource is changed (controller by ResourceGeneration), or when the finalizers are changed type ResourceGenerationOrFinalizerChangedPredicate struct { predicate.Funcs } // Update implements default UpdateEvent filter for validating resource version change func (ResourceGenerationOrFinalizerChangedPredicate) Update(e event.UpdateEvent) bool { if e.ObjectOld == nil { return false } if e.ObjectNew == nil { return false } if e.ObjectNew.GetGeneration() == e.ObjectOld.GetGeneration() && reflect.DeepEqual(e.ObjectNew.GetFinalizers(), e.ObjectOld.GetFinalizers()) { return false } return true } ================================================ FILE: pkg/util/reconciler.go ================================================ /* Copyright 2019 Red Hat, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "context" "errors" "io/ioutil" "os" "text/template" "time" "github.com/redhat-cop/operator-utils/pkg/util/apis" "github.com/redhat-cop/operator-utils/pkg/util/templates" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // ReconcilerBase is a base struct from which all reconcilers can be derived from. By doing so your reconcilers will also inherit a set of utility functions // To inherit from reconciler just build your finalizer this way: // // type MyReconciler struct { // util.ReconcilerBase // ... other optional fields ... // } type ReconcilerBase struct { apireader client.Reader client client.Client scheme *runtime.Scheme restConfig *rest.Config recorder record.EventRecorder } func NewReconcilerBase(client client.Client, scheme *runtime.Scheme, restConfig *rest.Config, recorder record.EventRecorder, apireader client.Reader) ReconcilerBase { return ReconcilerBase{ apireader: apireader, client: client, scheme: scheme, restConfig: restConfig, recorder: recorder, } } // NewReconcilerBase is a contruction function to create a new ReconcilerBase. func NewFromManager(mgr manager.Manager, recorder record.EventRecorder) ReconcilerBase { return NewReconcilerBase(mgr.GetClient(), mgr.GetScheme(), mgr.GetConfig(), recorder, mgr.GetAPIReader()) } // IsValid determines if a CR instance is valid. this implementation returns always true, should be overridden func (r *ReconcilerBase) IsValid(obj metav1.Object) (bool, error) { return true, nil } // IsInitialized determines if a CR instance is initialized. this implementation returns always true, should be overridden func (r *ReconcilerBase) IsInitialized(obj metav1.Object) bool { return true } // Reconcile is a stub function to have ReconsicerBase match the Reconciler interface. You must redefine this function func (r *ReconcilerBase) Reconcile(request reconcile.Request) (reconcile.Result, error) { return reconcile.Result{}, nil } // GetClient returns the underlying client func (r *ReconcilerBase) GetClient() client.Client { return r.client } // GetRestConfig returns the undelying rest config func (r *ReconcilerBase) GetRestConfig() *rest.Config { return r.restConfig } // GetRecorder returns the underlying recorder func (r *ReconcilerBase) GetRecorder() record.EventRecorder { return r.recorder } // GetScheme returns the scheme func (r *ReconcilerBase) GetScheme() *runtime.Scheme { return r.scheme } // GetDiscoveryClient returns a discovery client for the current reconciler func (r *ReconcilerBase) GetDiscoveryClient() (*discovery.DiscoveryClient, error) { return discovery.NewDiscoveryClientForConfig(r.GetRestConfig()) } // CreateOrUpdateResource creates a resource if it doesn't exist, and updates (overwrites it), if it exist // if owner is not nil, the owner field os set // if namespace is not "", the namespace field of the object is overwritten with the passed value func (r *ReconcilerBase) CreateOrUpdateResource(context context.Context, owner client.Object, namespace string, obj client.Object) error { log := log.FromContext(context) if owner != nil { _ = controllerutil.SetControllerReference(owner, obj, r.GetScheme()) } if namespace != "" { obj.SetNamespace(namespace) } obj2 := &unstructured.Unstructured{} obj2.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) err := r.GetClient().Get(context, types.NamespacedName{ Namespace: obj.GetNamespace(), Name: obj.GetName(), }, obj2) if apierrors.IsNotFound(err) { err = r.GetClient().Create(context, obj) if err != nil { log.Error(err, "unable to create object", "object", obj) return err } return nil } if err == nil { obj.SetResourceVersion(obj2.GetResourceVersion()) err = r.GetClient().Update(context, obj) if err != nil { log.Error(err, "unable to update object", "object", obj) return err } return nil } log.Error(err, "unable to lookup object", "object", obj) return err } // CreateOrUpdateResources operates as CreateOrUpdate, but on an array of resources func (r *ReconcilerBase) CreateOrUpdateResources(context context.Context, owner client.Object, namespace string, objs []client.Object) error { for _, obj := range objs { err := r.CreateOrUpdateResource(context, owner, namespace, obj) if err != nil { return err } } return nil } // CreateOrUpdateUnstructuredResources operates as CreateOrUpdate, but on an array of unstructured.Unstructured func (r *ReconcilerBase) CreateOrUpdateUnstructuredResources(context context.Context, owner client.Object, namespace string, objs []unstructured.Unstructured) error { for _, obj := range objs { err := r.CreateOrUpdateResource(context, owner, namespace, &obj) if err != nil { return err } } return nil } // DeleteResourceIfExists deletes an existing resource. It doesn't fail if the resource does not exist func (r *ReconcilerBase) DeleteResourceIfExists(context context.Context, obj client.Object) error { log := log.FromContext(context) err := r.GetClient().Delete(context, obj) if err != nil && !apierrors.IsNotFound(err) { log.Error(err, "unable to delete object ", "object", obj) return err } return nil } // DeleteResourcesIfExist operates like DeleteResources, but on an arrays of resources func (r *ReconcilerBase) DeleteResourcesIfExist(context context.Context, objs []client.Object) error { for _, obj := range objs { err := r.DeleteResourceIfExists(context, obj) if err != nil { return err } } return nil } // DeleteUnstructuredResources operates like DeleteResources, but on an arrays of unstructured.Unstructured func (r *ReconcilerBase) DeleteUnstructuredResources(context context.Context, objs []unstructured.Unstructured) error { for _, obj := range objs { err := r.DeleteResourceIfExists(context, &obj) if err != nil { return err } } return nil } // CreateResourceIfNotExists create a resource if it doesn't already exists. If the resource exists it is left untouched and the functin does not fails // if owner is not nil, the owner field os set // if namespace is not "", the namespace field of the object is overwritten with the passed value func (r *ReconcilerBase) CreateResourceIfNotExists(context context.Context, owner client.Object, namespace string, obj client.Object) error { log := log.FromContext(context) if owner != nil { _ = controllerutil.SetControllerReference(owner, obj, r.GetScheme()) } if namespace != "" { obj.SetNamespace(namespace) } err := r.GetClient().Create(context, obj) if err != nil && !apierrors.IsAlreadyExists(err) { log.Error(err, "unable to create object ", "object", obj) return err } return nil } // CreateResourcesIfNotExist operates as CreateResourceIfNotExists, but on an array of resources func (r *ReconcilerBase) CreateResourcesIfNotExist(context context.Context, owner client.Object, namespace string, objs []client.Object) error { for _, obj := range objs { err := r.CreateResourceIfNotExists(context, owner, namespace, obj) if err != nil { return err } } return nil } // CreateUnstructuredResourcesIfNotExist operates as CreateResourceIfNotExists, but on an array of unstructured.Unstructured func (r *ReconcilerBase) CreateUnstructuredResourcesIfNotExist(context context.Context, owner client.Object, namespace string, objs []unstructured.Unstructured) error { for _, obj := range objs { err := r.CreateResourceIfNotExists(context, owner, namespace, &obj) if err != nil { return err } } return nil } // CreateOrUpdateTemplatedResources processes an initialized template expecting an array of objects as a result and the processes them with the CreateOrUpdate function func (r *ReconcilerBase) CreateOrUpdateTemplatedResources(context context.Context, owner client.Object, namespace string, data interface{}, template *template.Template) error { log := log.FromContext(context) objs, err := templates.ProcessTemplateArray(context, data, template) if err != nil { log.Error(err, "error creating manifest from template") return err } for _, obj := range objs { err = r.CreateOrUpdateResource(context, owner, namespace, &obj) if err != nil { return err } } return nil } // CreateIfNotExistTemplatedResources processes an initialized template expecting an array of objects as a result and then processes them with the CreateResourceIfNotExists function func (r *ReconcilerBase) CreateIfNotExistTemplatedResources(context context.Context, owner client.Object, namespace string, data interface{}, template *template.Template) error { log := log.FromContext(context) objs, err := templates.ProcessTemplateArray(context, data, template) if err != nil { log.Error(err, "error creating manifest from template") return err } for _, obj := range objs { err = r.CreateResourceIfNotExists(context, owner, namespace, &obj) if err != nil { return err } } return nil } // DeleteTemplatedResources processes an initialized template expecting an array of objects as a result and then processes them with the Delete function func (r *ReconcilerBase) DeleteTemplatedResources(context context.Context, data interface{}, template *template.Template) error { log := log.FromContext(context) objs, err := templates.ProcessTemplateArray(context, data, template) if err != nil { log.Error(err, "error creating manifest from template") return err } for _, obj := range objs { err = r.DeleteResourceIfExists(context, &obj) if err != nil { return err } } return nil } // ManageOutcomeWithRequeue is a convenience function to call either ManageErrorWithRequeue if issue is non-nil, else ManageSuccessWithRequeue func (r *ReconcilerBase) ManageOutcomeWithRequeue(context context.Context, obj client.Object, issue error, requeueAfter time.Duration) (reconcile.Result, error) { if issue != nil { return r.ManageErrorWithRequeue(context, obj, issue, requeueAfter) } return r.ManageSuccessWithRequeue(context, obj, requeueAfter) } // ManageErrorWithRequeue will take care of the following: // 1. generate a warning event attached to the passed CR // 2. set the status of the passed CR to a error condition if the object implements the apis.ConditionsStatusAware interface // 3. return a reconcile status with with the passed requeueAfter and error func (r *ReconcilerBase) ManageErrorWithRequeue(context context.Context, obj client.Object, issue error, requeueAfter time.Duration) (reconcile.Result, error) { log := log.FromContext(context) r.GetRecorder().Event(obj, "Warning", "ProcessingError", issue.Error()) if conditionsAware, updateStatus := (obj).(apis.ConditionsAware); updateStatus { condition := metav1.Condition{ Type: apis.ReconcileError, LastTransitionTime: metav1.Now(), ObservedGeneration: obj.GetGeneration(), Message: issue.Error(), Reason: apis.ReconcileErrorReason, Status: metav1.ConditionTrue, } conditionsAware.SetConditions(apis.AddOrReplaceCondition(condition, conditionsAware.GetConditions())) err := r.GetClient().Status().Update(context, obj) if err != nil { log.Error(err, "unable to update status") return reconcile.Result{RequeueAfter: requeueAfter}, err } } else { log.V(1).Info("object is not ConditionsAware, not setting status") } return reconcile.Result{RequeueAfter: requeueAfter}, issue } // ManageError will take care of the following: // 1. generate a warning event attached to the passed CR // 2. set the status of the passed CR to a error condition if the object implements the apis.ConditionsStatusAware interface // 3. return a reconcile status with the passed error func (r *ReconcilerBase) ManageError(context context.Context, obj client.Object, issue error) (reconcile.Result, error) { return r.ManageErrorWithRequeue(context, obj, issue, 0) } // ManageSuccessWithRequeue will update the status of the CR and return a successful reconcile result with requeueAfter set func (r *ReconcilerBase) ManageSuccessWithRequeue(context context.Context, obj client.Object, requeueAfter time.Duration) (reconcile.Result, error) { log := log.FromContext(context) if conditionsAware, updateStatus := (obj).(apis.ConditionsAware); updateStatus { condition := metav1.Condition{ Type: apis.ReconcileSuccess, LastTransitionTime: metav1.Now(), ObservedGeneration: obj.GetGeneration(), Reason: apis.ReconcileSuccessReason, Status: metav1.ConditionTrue, } conditionsAware.SetConditions(apis.AddOrReplaceCondition(condition, conditionsAware.GetConditions())) err := r.GetClient().Status().Update(context, obj) if err != nil { log.Error(err, "unable to update status") return reconcile.Result{RequeueAfter: requeueAfter}, err } } else { log.V(1).Info("object is not ConditionsAware, not setting status") } return reconcile.Result{RequeueAfter: requeueAfter}, nil } // ManageSuccess will update the status of the CR and return a successful reconcile result func (r *ReconcilerBase) ManageSuccess(context context.Context, obj client.Object) (reconcile.Result, error) { return r.ManageSuccessWithRequeue(context, obj, 0) } // GetDirectClient returns a non cached client func (r *ReconcilerBase) GetDirectClient() (client.Client, error) { return r.GetDirectClientWithSchemeBuilders() } // GetDirectClientWithSchemeBuilders returns a non cached client initialized with the scheme.buidlers passed as parameters func (r *ReconcilerBase) GetDirectClientWithSchemeBuilders(addToSchemes ...func(s *runtime.Scheme) error) (client.Client, error) { scheme := runtime.NewScheme() for _, addToscheme := range append(addToSchemes, clientgoscheme.AddToScheme) { err := addToscheme(scheme) if err != nil { return nil, err } } return client.New(r.GetRestConfig(), client.Options{ Scheme: scheme, }) } // GetAPIReader returns a non cached reader func (r *ReconcilerBase) GetAPIReader() client.Reader { return r.apireader } // GetOperatorNamespace tries to infer the operator namespace. I first looks for the /var/run/secrets/kubernetes.io/serviceaccount/namespace file. // Then it looks for a NAMESPACE environment variable (useful when running in local mode). func (r *ReconcilerBase) GetOperatorNamespace() (string, error) { var namespaceFilePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" b, err := ioutil.ReadFile(namespaceFilePath) if err != nil { namespace, ok := os.LookupEnv("NAMESPACE") if !ok { return "", errors.New("unable to infer namespace in which operator is running") } return namespace, nil } return string(b), nil } ================================================ FILE: pkg/util/stoppablemanager/stoppable-manager.go ================================================ package stoppablemanager import ( "context" "errors" "k8s.io/client-go/rest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" ) var log = logf.Log.WithName("stoppable_manager") // StoppableManager A StoppableManaager allows you to easily create controller-runtim.Managers that can be started and stopped. type StoppableManager struct { started bool manager.Manager cancelFunction context.CancelFunc } // Stop stops the manager func (sm *StoppableManager) Stop() { if !sm.started { log.Error(errors.New("invalid argument"), "stop called on a non started channel", "started", sm.started) return } sm.cancelFunction() //close(sm.stopChannel) sm.started = false } // Start starts the manager. Restarting a starated manager is a noop that will be logged. func (sm *StoppableManager) Start(parentCtx context.Context) { if sm.started { log.Error(errors.New("invalid argument"), "start called on a started channel") return } ctx, cancel := context.WithCancel(parentCtx) sm.cancelFunction = cancel go func() { err := sm.Manager.Start(ctx) if err != nil { log.Error(errors.New("unable to start manager"), "unable to start manager") } }() sm.started = true } // NewStoppableManager creates a new stoppable manager func NewStoppableManager(config *rest.Config, options manager.Options) (StoppableManager, error) { manager, err := manager.New(config, options) if err != nil { return StoppableManager{}, err } return StoppableManager{ Manager: manager, }, nil } // IsStarted returns wether this stoppable manager is running. func (sm *StoppableManager) IsStarted() bool { return sm.started } ================================================ FILE: pkg/util/templates/advanced-funcmap.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* Concept pulled from Helm to match a known templating pattern https://github.com/helm/helm/blob/master/pkg/engine/funcs.go */ package templates import ( "bytes" "context" "encoding/json" "strings" "text/template" "github.com/BurntSushi/toml" "github.com/Masterminds/sprig/v3" "github.com/go-logr/logr" "github.com/pkg/errors" "github.com/redhat-cop/operator-utils/pkg/util/dynamicclient" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) // AdvancedTemplateFuncMap to add Sprig and additional templating functions func AdvancedTemplateFuncMap(config *rest.Config, logger logr.Logger) template.FuncMap { f := sprig.HermeticTxtFuncMap() // Removed these functions from the core Sprig package for security concerns delete(f, "env") delete(f, "expandenv") extra := template.FuncMap{ "toToml": toTOML, "toYaml": toYAML, "fromYaml": fromYAML, "fromYamlArray": fromYAMLArray, "toJson": toJSON, "fromJson": fromJSON, "fromJsonArray": fromJSONArray, // A variety of known templating functions that have not been implemented yet "include": func(string, interface{}) string { return "not implemented" }, "tpl": func(string, interface{}) interface{} { return "not implemented" }, "required": func(string, interface{}) (interface{}, error) { return "not implemented", nil }, } for k, v := range extra { f[k] = v } // Adding additional functionality found in Helm f["lookup"] = NewLookupFunction(config, logger) // Add the `required` function here so we can use lintMode f["required"] = func(warn string, val interface{}) (interface{}, error) { if val == nil { return val, errors.Errorf(warn) } else if _, ok := val.(string); ok { if val == "" { return val, errors.Errorf(warn) } } return val, nil } return f } // toYAML takes an interface, marshals it to yaml, and returns a string. It will // always return a string, even on marshal error (empty string). // // This is designed to be called from a template. func toYAML(v interface{}) string { data, err := yaml.Marshal(v) if err != nil { // Swallow errors inside of a template. return "" } return strings.TrimSuffix(string(data), "\n") } // fromYAML converts a YAML document into a map[string]interface{}. // // This is not a general-purpose YAML parser, and will not parse all valid // YAML documents. Additionally, because its intended use is within templates // it tolerates errors. It will insert the returned error message string into // m["Error"] in the returned map. func fromYAML(str string) map[string]interface{} { m := map[string]interface{}{} if err := yaml.Unmarshal([]byte(str), &m); err != nil { m["Error"] = err.Error() } return m } // fromYAMLArray converts a YAML array into a []interface{}. // // This is not a general-purpose YAML parser, and will not parse all valid // YAML documents. Additionally, because its intended use is within templates // it tolerates errors. It will insert the returned error message string as // the first and only item in the returned array. func fromYAMLArray(str string) []interface{} { a := []interface{}{} if err := yaml.Unmarshal([]byte(str), &a); err != nil { a = []interface{}{err.Error()} } return a } // toTOML takes an interface, marshals it to toml, and returns a string. It will // always return a string, even on marshal error (empty string). // // This is designed to be called from a template. func toTOML(v interface{}) string { b := bytes.NewBuffer(nil) e := toml.NewEncoder(b) err := e.Encode(v) if err != nil { return err.Error() } return b.String() } // toJSON takes an interface, marshals it to json, and returns a string. It will // always return a string, even on marshal error (empty string). // // This is designed to be called from a template. func toJSON(v interface{}) string { data, err := json.Marshal(v) if err != nil { // Swallow errors inside of a template. return "" } return string(data) } // fromJSON converts a JSON document into a map[string]interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid // JSON documents. Additionally, because its intended use is within templates // it tolerates errors. It will insert the returned error message string into // m["Error"] in the returned map. func fromJSON(str string) map[string]interface{} { m := make(map[string]interface{}) if err := json.Unmarshal([]byte(str), &m); err != nil { m["Error"] = err.Error() } return m } // fromJSONArray converts a JSON array into a []interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid // JSON documents. Additionally, because its intended use is within templates // it tolerates errors. It will insert the returned error message string as // the first and only item in the returned array. func fromJSONArray(str string) []interface{} { a := []interface{}{} if err := json.Unmarshal([]byte(str), &a); err != nil { a = []interface{}{err.Error()} } return a } type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) // NewLookupFunction get information at runtime from cluster func NewLookupFunction(config *rest.Config, logger logr.Logger) lookupFunc { return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { var client dynamic.ResourceInterface ctx := context.TODO() ctx = context.WithValue(ctx, "restConfig", config) ctx = log.IntoContext(ctx, logger.WithName("lookup function")) c, namespaced, err := dynamicclient.GetDynamicClientForGVK(ctx, schema.FromAPIVersionAndKind(apiversion, resource)) if err != nil { return map[string]interface{}{}, err } if namespaced && namespace != "" { client = c.Namespace(namespace) } else { client = c } if name != "" { // this will return a single object obj, err := client.Get(ctx, name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. // That way, users can use `if not (lookup ...)` in their templates. return map[string]interface{}{}, nil } return map[string]interface{}{}, err } return obj.UnstructuredContent(), nil } //this will return a list obj, err := client.List(ctx, metav1.ListOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. // That way, users can use `if not (lookup ...)` in their templates. return map[string]interface{}{}, nil } return map[string]interface{}{}, err } return obj.UnstructuredContent(), nil } } ================================================ FILE: pkg/util/templates/templates.go ================================================ /* Copyright 2019 Red Hat, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package templates import ( "bytes" "context" "encoding/json" "text/template" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/kubectl/pkg/validation" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) // ProcessTemplate processes an initialized Go template with a set of data. It expects one API object to be defined in the template // requires a context with log func ProcessTemplate(context context.Context, data interface{}, template *template.Template) (*unstructured.Unstructured, error) { log := log.FromContext(context) obj := unstructured.Unstructured{} var b bytes.Buffer err := template.Execute(&b, data) if err != nil { log.Error(err, "Error executing template", "template", template) return &obj, err } bb, err := yaml.YAMLToJSON(b.Bytes()) if err != nil { log.Error(err, "Error transforming yaml to json", "manifest", b.String()) return &obj, err } err = obj.UnmarshalJSON(bb) if err != nil { log.Error(err, "Error unmarshalling json manifest", "manifest", string(bb)) return &obj, err } return &obj, err } // ProcessTemplateArray processes an initialized Go template with a set of data. It expects an arrays of API objects to be defined in the template. Dishomogeneus types are supported // requires a context with log func ProcessTemplateArray(context context.Context, data interface{}, template *template.Template) ([]unstructured.Unstructured, error) { log := log.FromContext(context) objs := []unstructured.Unstructured{} var b bytes.Buffer err := template.Execute(&b, data) if err != nil { log.Error(err, "Error executing template", "template", template) return []unstructured.Unstructured{}, err } bb, err := yaml.YAMLToJSON(b.Bytes()) if err != nil { log.Error(err, "Error transforming yaml to json", "manifest", b.String()) return []unstructured.Unstructured{}, err } if !IsJSONArray(bb) { obj := unstructured.Unstructured{} err = obj.UnmarshalJSON(bb) objs = append(objs, obj) } else { intfs := &[]interface{}{} err = json.Unmarshal(bb, &intfs) if err != nil { log.Error(err, "Error unmarshalling json manifest", "manifest", string(bb)) return []unstructured.Unstructured{}, err } for _, intf := range *intfs { b, err := json.Marshal(intf) if err != nil { log.Error(err, "Error marshalling", "interface", intf) return []unstructured.Unstructured{}, err } obj := unstructured.Unstructured{} err = obj.UnmarshalJSON(b) if err != nil { log.Error(err, "Error unmarshalling", "json", string(b)) return []unstructured.Unstructured{}, err } objs = append(objs, obj) } } if err != nil { log.Error(err, "Error unmarshalling json manifest", "manifest", string(bb)) return []unstructured.Unstructured{}, err } return objs, err } // ValidateUnstructured validates the content of an unstructured against an openapi schema. // the schema is intended to be retrieved from a running instance of kubernetes, but other usages are possible. // requires a context with log func ValidateUnstructured(context context.Context, obj *unstructured.Unstructured, validationSchema validation.Schema) error { log := log.FromContext(context) bb, err := obj.MarshalJSON() if err != nil { log.Error(err, "unable to unmarshall", "unstructured", obj) return err } err = validationSchema.ValidateBytes(bb) if err != nil { log.Error(err, "unable to validate", "json doc", string(bb), "against schemas", validationSchema) return err } return nil } // IsJSONArray checks to see if a byte array containing JSON is an array of data func IsJSONArray(data []byte) bool { firstLine := bytes.TrimLeft(data, " \t\r\n") return firstLine[0] == '[' } ================================================ FILE: test/enforcing-patch-multiple-cluster-level.yaml ================================================ # test multiple instances cluster apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingPatch metadata: name: test-patch-multiple-cluster-level spec: patches: - name: ciao1 targetObjectRef: apiVersion: v1 kind: Namespace patchTemplate: | metadata: annotations: {{ (index . 0).metadata.uid }}: {{ (index . 1) }} patchType: application/strategic-merge-patch+json sourceObjectRefs: - apiVersion: v1 kind: ServiceAccount name: default namespace: "{{ .metadata.name }}" fieldPath: $.metadata.uid ================================================ FILE: test/enforcing-patch-multiple.yaml ================================================ #test multiple instances namespaced apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingPatch metadata: name: test-patch-multiple spec: patches: - name: ciao1 targetObjectRef: apiVersion: v1 kind: ServiceAccount name: deployer patchTemplate: | metadata: annotations: {{ (index . 0).metadata.uid }}: {{ (index . 1) }} patchType: application/strategic-merge-patch+json sourceObjectRefs: - apiVersion: v1 kind: ServiceAccount name: default namespace: "{{ .metadata.namespace }}" fieldPath: $.metadata.uid ================================================ FILE: test/enforcing-patch.yaml ================================================ # # test single instance namespaced apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingPatch metadata: name: test-field-patch spec: patches: test-field-patch: targetObjectRef: apiVersion: v1 kind: ServiceAccount name: test namespace: patch-test patchTemplate: | metadata: annotations: {{ (index . 1) }}: {{ (index . 2) }} patchType: application/strategic-merge-patch+json sourceObjectRefs: - apiVersion: v1 kind: Namespace name: default fieldPath: $.metadata.uid - apiVersion: v1 kind: ServiceAccount name: default namespace: default fieldPath: $.metadata.uid ================================================ FILE: test/enforcing_cr.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingCRD metadata: name: example-enforcingcrd spec: resources: - object: apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2020-03-30T16:24:08Z" name: test-configmap namespace: test-enforcingcrd data: ciao: ciao - object: apiVersion: route.openshift.io/v1 kind: Route metadata: name: test-route namespace: test-enforcingcrd spec: host: grafana-istio-system.apps.cluster-4cac.sandbox456.opentlc.com tls: termination: reencrypt to: kind: Service name: grafana weight: 100 wildcardPolicy: None ================================================ FILE: test/failing-enforcing_cr.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: EnforcingCRD metadata: name: example-enforcingcrd2 spec: resources: - object: apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2020-03-30T16:24:08Z" name: test-configmap-FALING namespace: test-enforcingcrd data: ciao: ciao - object: apiVersion: route.openshift.io/v1 kind: Route metadata: name: test-route-FAILING namespace: test-enforcingcrd spec: host: grafana-istio-system.apps.cluster-4cac.sandbox456.opentlc.com tls: termination: reencrypt to: kind: Service name: grafana weight: 100 wildcardPolicy: None ================================================ FILE: test/mycrd_cr.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: MyCRD metadata: name: example-mycrd spec: # Add fields here initialized: false valid: true error: false ================================================ FILE: test/templatedenforcing_cr.yaml ================================================ apiVersion: operator-utils.example.io/v1alpha1 kind: TemplatedEnforcingCRD metadata: name: example-enforcingcrd spec: templates: - objectTemplate: | apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2020-03-30T16:24:08Z" name: test-configmap namespace: {{ .Namespace }} data: ciao: ciao - objectTemplate: | apiVersion: route.openshift.io/v1 kind: Route metadata: name: test-route namespace: {{ .Namespace }} spec: host: grafana-istio-system.apps.cluster-4cac.sandbox456.opentlc.com tls: termination: reencrypt to: kind: Service name: grafana weight: 100 wildcardPolicy: None ================================================ FILE: testbin/setup-envtest.sh ================================================ #!/usr/bin/env bash # Copyright 2020 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o errexit set -o pipefail # Turn colors in this script off by setting the NO_COLOR variable in your # environment to any value: # # $ NO_COLOR=1 test.sh NO_COLOR=${NO_COLOR:-""} if [ -z "$NO_COLOR" ]; then header=$'\e[1;33m' reset=$'\e[0m' else header='' reset='' fi function header_text { echo "$header$*$reset" } function setup_envtest_env { header_text "setting up env vars" # Setup env vars KUBEBUILDER_ASSETS=${KUBEBUILDER_ASSETS:-""} if [[ -z "${KUBEBUILDER_ASSETS}" ]]; then export KUBEBUILDER_ASSETS=$1/bin fi } # fetch k8s API gen tools and make it available under envtest_root_dir/bin. # # Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable # in your environment to any value: # # $ SKIP_FETCH_TOOLS=1 ./check-everything.sh # # If you skip fetching tools, this script will use the tools already on your # machine. function fetch_envtest_tools { SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""} if [ -n "$SKIP_FETCH_TOOLS" ]; then return 0 fi tmp_root=/tmp envtest_root_dir=$tmp_root/envtest k8s_version="${ENVTEST_K8S_VERSION:-1.16.4}" goarch="$(go env GOARCH)" goos="$(go env GOOS)" if [[ "$goos" != "linux" && "$goos" != "darwin" ]]; then echo "OS '$goos' not supported. Aborting." >&2 return 1 fi local dest_dir="${1}" # use the pre-existing version in the temporary folder if it matches our k8s version if [[ -x "${dest_dir}/bin/kube-apiserver" ]]; then version=$("${dest_dir}"/bin/kube-apiserver --version) if [[ $version == *"${k8s_version}"* ]]; then header_text "Using cached envtest tools from ${dest_dir}" return 0 fi fi header_text "fetching envtest tools@${k8s_version} (into '${dest_dir}')" envtest_tools_archive_name="kubebuilder-tools-$k8s_version-$goos-$goarch.tar.gz" envtest_tools_download_url="https://storage.googleapis.com/kubebuilder-tools/$envtest_tools_archive_name" envtest_tools_archive_path="$tmp_root/$envtest_tools_archive_name" if [ ! -f $envtest_tools_archive_path ]; then curl -sL ${envtest_tools_download_url} -o "$envtest_tools_archive_path" fi mkdir -p "${dest_dir}" tar -C "${dest_dir}" --strip-components=1 -zvxf "$envtest_tools_archive_path" }