Repository: rancher/wrangler Branch: main Commit: b8c7650c8e9f Files: 237 Total size: 933.1 KB Directory structure: gitextract_a9xsblpa/ ├── .github/ │ ├── renovate.json │ └── workflows/ │ ├── ci.yaml │ ├── fossa.yml │ ├── release.yaml │ └── renovate-vault.yml ├── .gitignore ├── .golangci.json ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── VERSION.md ├── codegen.go ├── go.mod ├── go.sum ├── pkg/ │ ├── apply/ │ │ ├── apply.go │ │ ├── client_factory.go │ │ ├── desiredset.go │ │ ├── desiredset_apply.go │ │ ├── desiredset_compare.go │ │ ├── desiredset_compare_test.go │ │ ├── desiredset_crud.go │ │ ├── desiredset_owner.go │ │ ├── desiredset_process.go │ │ ├── desiredset_process_test.go │ │ ├── fake/ │ │ │ └── apply.go │ │ ├── injectors/ │ │ │ └── registry.go │ │ └── reconcilers.go │ ├── broadcast/ │ │ └── generic.go │ ├── cleanup/ │ │ └── cleanup.go │ ├── clients/ │ │ └── clients.go │ ├── codegen/ │ │ └── main.go │ ├── condition/ │ │ └── condition.go │ ├── controller-gen/ │ │ ├── OWNERS │ │ ├── README.md │ │ ├── args/ │ │ │ ├── args.go │ │ │ ├── groupversion.go │ │ │ ├── groupversion_test.go │ │ │ └── testdata/ │ │ │ └── test.go │ │ ├── generators/ │ │ │ ├── client_generator.go │ │ │ ├── factory_go.go │ │ │ ├── group_interface_go.go │ │ │ ├── group_version_interface_go.go │ │ │ ├── list_type_go.go │ │ │ ├── register_group_go.go │ │ │ ├── register_group_version_go.go │ │ │ ├── target.go │ │ │ ├── type_go.go │ │ │ └── util.go │ │ └── main.go │ ├── crd/ │ │ ├── crd.go │ │ ├── crd_test.go │ │ ├── init.go │ │ ├── mockCRDClient_test.go │ │ └── print.go │ ├── data/ │ │ ├── convert/ │ │ │ ├── convert.go │ │ │ └── convert_test.go │ │ ├── data.go │ │ ├── merge.go │ │ ├── values.go │ │ └── values_test.go │ ├── generated/ │ │ └── controllers/ │ │ ├── admissionregistration.k8s.io/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── interface.go │ │ │ ├── mutatingwebhookconfiguration.go │ │ │ └── validatingwebhookconfiguration.go │ │ ├── apiextensions.k8s.io/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── customresourcedefinition.go │ │ │ └── interface.go │ │ ├── apiregistration.k8s.io/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── apiservice.go │ │ │ └── interface.go │ │ ├── apps/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── daemonset.go │ │ │ ├── deployment.go │ │ │ ├── interface.go │ │ │ └── statefulset.go │ │ ├── batch/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── interface.go │ │ │ └── job.go │ │ ├── coordination.k8s.io/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── interface.go │ │ │ └── lease.go │ │ ├── core/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── configmap.go │ │ │ ├── endpoints.go │ │ │ ├── event.go │ │ │ ├── interface.go │ │ │ ├── limitrange.go │ │ │ ├── namespace.go │ │ │ ├── node.go │ │ │ ├── persistentvolume.go │ │ │ ├── persistentvolumeclaim.go │ │ │ ├── pod.go │ │ │ ├── resourcequota.go │ │ │ ├── secret.go │ │ │ ├── service.go │ │ │ └── serviceaccount.go │ │ ├── discovery/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── endpointslice.go │ │ │ └── interface.go │ │ ├── extensions/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1beta1/ │ │ │ ├── ingress.go │ │ │ └── interface.go │ │ ├── networking.k8s.io/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── interface.go │ │ │ └── networkpolicy.go │ │ ├── rbac/ │ │ │ ├── factory.go │ │ │ ├── interface.go │ │ │ └── v1/ │ │ │ ├── clusterrole.go │ │ │ ├── clusterrolebinding.go │ │ │ ├── interface.go │ │ │ ├── role.go │ │ │ └── rolebinding.go │ │ └── storage/ │ │ ├── factory.go │ │ ├── interface.go │ │ └── v1/ │ │ ├── interface.go │ │ └── storageclass.go │ ├── generic/ │ │ ├── cache.go │ │ ├── cache_test.go │ │ ├── clientMocks_test.go │ │ ├── controller.go │ │ ├── controllerFactoryMocks_test.go │ │ ├── controller_test.go │ │ ├── embeddedClient.go │ │ ├── factory.go │ │ ├── fake/ │ │ │ ├── README.md │ │ │ ├── cache.go │ │ │ ├── controller.go │ │ │ ├── fake_test.go │ │ │ └── generate.go │ │ ├── generating.go │ │ ├── generating_test.go │ │ ├── remove.go │ │ └── remove_test.go │ ├── genericcondition/ │ │ └── condition.go │ ├── gvk/ │ │ ├── detect.go │ │ └── get.go │ ├── k8scheck/ │ │ └── wait.go │ ├── kstatus/ │ │ └── kstatus.go │ ├── kubeconfig/ │ │ └── loader.go │ ├── kv/ │ │ └── split.go │ ├── leader/ │ │ ├── leader.go │ │ ├── leader_test.go │ │ └── manager.go │ ├── merr/ │ │ └── error.go │ ├── name/ │ │ ├── name.go │ │ └── name_test.go │ ├── needacert/ │ │ ├── needacert.go │ │ └── needacert_test.go │ ├── objectset/ │ │ ├── objectset.go │ │ └── objectset_test.go │ ├── patch/ │ │ ├── apply.go │ │ └── style.go │ ├── randomtoken/ │ │ └── token.go │ ├── ratelimit/ │ │ └── none.go │ ├── relatedresource/ │ │ ├── all.go │ │ ├── changeset.go │ │ ├── changeset_test.go │ │ └── owner.go │ ├── resolvehome/ │ │ └── main.go │ ├── schemas/ │ │ ├── definition/ │ │ │ └── definition.go │ │ ├── mapper.go │ │ ├── mappers/ │ │ │ ├── access.go │ │ │ ├── alias.go │ │ │ ├── check.go │ │ │ ├── condition.go │ │ │ ├── copy.go │ │ │ ├── default.go │ │ │ ├── drop.go │ │ │ ├── embed.go │ │ │ ├── empty.go │ │ │ ├── enum.go │ │ │ ├── exists.go │ │ │ ├── json_keys.go │ │ │ ├── metadata.go │ │ │ ├── move.go │ │ │ ├── set_value.go │ │ │ └── slice_to_map.go │ │ ├── openapi/ │ │ │ └── generate.go │ │ ├── reflection.go │ │ ├── schemas.go │ │ ├── types.go │ │ └── validation/ │ │ ├── error.go │ │ └── validation.go │ ├── schemes/ │ │ └── all.go │ ├── seen/ │ │ └── strings.go │ ├── signals/ │ │ ├── signal.go │ │ ├── signal_posix.go │ │ └── signal_windows.go │ ├── slice/ │ │ └── contains.go │ ├── start/ │ │ └── all.go │ ├── stringset/ │ │ ├── stringset.go │ │ └── stringset_test.go │ ├── summary/ │ │ ├── capi_cluster_test.go │ │ ├── capi_machine_test.go │ │ ├── capi_machineset_test.go │ │ ├── cattletypes.go │ │ ├── cattletypes_test.go │ │ ├── client/ │ │ │ ├── interface.go │ │ │ ├── options.go │ │ │ └── simple.go │ │ ├── condition.go │ │ ├── condition_test.go │ │ ├── coretypes.go │ │ ├── gvk.go │ │ ├── gvk_test.go │ │ ├── informer/ │ │ │ ├── informer.go │ │ │ ├── informer_test.go │ │ │ ├── interface.go │ │ │ └── watchlist.go │ │ ├── lister/ │ │ │ ├── interface.go │ │ │ ├── lister.go │ │ │ └── shim.go │ │ ├── summarized.go │ │ ├── summarizers.go │ │ ├── summarizers_test.go │ │ └── summary.go │ ├── ticker/ │ │ └── ticker.go │ ├── trigger/ │ │ └── evalall.go │ ├── unstructured/ │ │ └── unstructured.go │ ├── webhook/ │ │ ├── match.go │ │ └── router.go │ └── yaml/ │ ├── objects_test.go │ ├── yaml.go │ └── yaml_test.go └── scripts/ ├── boilerplate.go.txt └── ci ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/renovate.json ================================================ { "extends": [ "github>rancher/renovate-config#release" ], "baseBranchPatterns": [ "main", "release/v2" ], "prHourlyLimit": 2, "packageRules": [ { "enabled": false, "matchPackageNames": [ "/k8s.io/*/", "/sigs.k8s.io/*/", "/go.opentelemetry.io/*/", "/github.com/prometheus/*/" ] }, { "matchUpdateTypes": [ "major", "minor" ], "enabled": false, "matchPackageNames": [ "/github.com/rancher/lasso/*/" ] } ] } ================================================ FILE: .github/workflows/ci.yaml ================================================ name: Wrangler CI on: push: pull_request: tags: - v* branches: - 'release/*' - 'main' jobs: ci: strategy: matrix: arch: - amd64 - arm64 runs-on: org-${{ github.repository_owner_id }}-${{ matrix.arch }}-k8s container: registry.suse.com/bci/golang:1.25 steps: - name : Checkout repository # https://github.com/actions/checkout/releases/tag/v4.1.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name : Install mockgen run: go install -v -x go.uber.org/mock/mockgen@v0.6.0 - name : Run CI run: bash scripts/ci golangci: name: golangci-lint runs-on: ubuntu-latest env: SETUP_GO_VERSION: '^1.25' GOLANG_CI_LINT_VERSION: v2.7.1 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: submodules: recursive - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ env.SETUP_GO_VERSION }} - name: Generate Golang run: | export PATH=$PATH:/home/runner/go/bin/ - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: ${{ env.GOLANG_CI_LINT_VERSION }} ================================================ FILE: .github/workflows/fossa.yml ================================================ name: FOSSA Scanning on: push: branches: ["main", "master", "release/**"] workflow_dispatch: permissions: contents: read id-token: write jobs: fossa-scanning: runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # The FOSSA token is shared between all repos in Rancher's GH org. It can be # used directly and there is no need to request specific access to EIO. - name: Read FOSSA token uses: rancher-eio/read-vault-secrets@0da85151ad1f19ed7986c41587e45aac1ace74b6 # v3 with: secrets: | secret/data/github/org/rancher/fossa/push token | FOSSA_API_KEY_PUSH_ONLY - name: FOSSA scan uses: fossas/fossa-action@c414b9ad82eaad041e47a7cf62a4f02411f427a0 # v1.8.0 with: api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }} # Only runs the scan and do not provide/returns any results back to the # pipeline. run-tests: false ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: push: tags: - v* jobs: release: runs-on: ubuntu-latest permissions: contents: write steps: - name : Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Create release on Github env: GH_TOKEN: ${{ github.token }} run: | if [[ "${{ github.ref_name }}" == *-rc* ]]; then gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes --prerelease else gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes fi ================================================ FILE: .github/workflows/renovate-vault.yml ================================================ name: Renovate on: workflow_dispatch: inputs: logLevel: description: "Override default log level" required: false default: "info" type: string overrideSchedule: description: "Override all schedules" required: false default: "false" type: string # Run twice in the early morning (UTC) for initial and follow up steps (create pull request and merge) schedule: - cron: '30 4,6 * * *' permissions: contents: read id-token: write jobs: call-workflow: uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@84bf074154364f80af052ebba8e23614212b79df # release with: logLevel: ${{ inputs.logLevel || 'info' }} overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }} secrets: inherit ================================================ FILE: .gitignore ================================================ /.dapper /bin /dist /build *.swp /.trash-cache /trash.lock /.idea /package/rancher /package/agent /tests/MANIFEST /tests/integration/MANIFEST tests/integration/MANIFEST tests/integration/.idea/ /tests/.cache /tests/.tox /tests/integration/.tox/ /tests/.venv /tests/.idea /default.etcd *.pyc __pycache__ /management-state /rancher *.pytest_cache .kube/ .vscode/ .DS_Store tests/validation/.idea all.yaml kustomization.yaml ================================================ FILE: .golangci.json ================================================ { "formatters": { "enable": [ "gofmt", "goimports" ], "exclusions": { "generated": "lax", "paths": [ "vendor", "tests", "pkg/client", "pkg/generated", "third_party$", "builtin$", "examples$" ] }, "settings": { "gofmt": { "simplify": false } } }, "linters": { "default": "none", "enable": [ "govet", "ineffassign", "misspell", "revive" ], "exclusions": { "generated": "lax", "paths": [ "vendor", "tests", "pkg/client", "pkg/generated", "third_party$", "builtin$", "examples$" ], "presets": [ "comments", "common-false-positives", "legacy", "std-error-handling" ], "rules": [ { "linters": [ "govet" ], "text": "^(nilness|structtag)" }, { "path": "pkg/apis/management.cattle.io/v3/globaldns_types.go", "text": ".*lobalDns.*" }, { "path": "pkg/apis/management.cattle.io/v3/zz_generated_register.go", "text": ".*lobalDns.*" }, { "path": "pkg/apis/management.cattle.io/v3/zz_generated_list_types.go", "text": ".*lobalDns.*" }, { "linters": [ "revive" ], "text": "should have comment" }, { "linters": [ "revive" ], "text": "should be of the form" }, { "linters": [ "revive" ], "text": "by other packages, and that stutters" }, { "linters": [ "revive" ], "text": "unused-parameter" }, { "linters": [ "revive" ], "text": "redefines-builtin-id" }, { "linters": [ "revive" ], "text": "superfluous-else" }, { "linters": [ "revive" ], "text": "empty-block" }, { "linters": [ "revive" ], "text": "if-return: redundant if" }, { "linters": [ "revive" ], "text": "var-naming: avoid meaningless package names" } ] } }, "run": { "tests": false }, "version": "2" } ================================================ FILE: CODEOWNERS ================================================ * @rancher/rancher-squad-frameworks ================================================ 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 ================================================ FILE: Makefile ================================================ all: generate validate build generate: go generate validate: go fmt ./... go vet ./... build: go build ./... ================================================ FILE: README.md ================================================ # Wrangler Most people writing controllers are a bit lost as they find that there is nothing in Kubernetes that is like `type Controller interface` where you can just do `NewController`. Instead a controller is really just a pattern of how you use the generated clientsets, informers, and listers combined with some custom event handlers and a workqueue. Wrangler is a framework for using controllers. Controllers wrap clients, informers, listers into a simple usable controller pattern that promotes some good practices.
## Some Projects that use Wrangler [rancher](https://github.com/rancher/rancher) [eks-operator](https://github.com/rancher/eks-operator) [aks-operator](https://github.com/rancher/aks-operator) [gke-operator](https://github.com/rancher/gke-operator) ## Versioning and Updates Wrangler releases use [semantic versioning](https://semver.org/). New major releases are created for breaking changes, new minor releases are created for features, and patches are added for everything else. The most recent Major.Minor.x release and any releases being used by the most recent patch version of a [supported rancher version](https://www.suse.com/lifecycle/#rancher) will be maintained. The most recent major will receive minor releases, along with patch releases on its most up to date minor release. Older Major.Minor.x releases still in use by rancher will receive security patches at minimum. Consequently, there will be 1-3 maintained releases of the form Major.Minor.x at a time. Currently maintained versions: | Wrangler Version | Rancher Version | Update Level | | ---------------- | --------------- | ---------------------- | | 1.0.x | 2.6.x | Security Fixes | | 1.1.x | 2.7.x | Bug and Security Fixes | Wrangler releases are not from the default branch. Instead they are from branches with the naming pattern `release-MAJOR.MINOR`. The default branch (i.e. master) is where changes initially go. This includes bug fixes and new features. Bug fixes are cherry-picked to release branches to be included in patch releases. When it's time to create a new minor or major release, a new release branch is created from the default branch.
# Table of Contents 1. [How it Works](#How-it-works) 1. [Useful Definitions](#useful-definitions) 2. [How to Use Wrangler](#how-to-use-wrangler) 1. [How to Write and Register a Handler](#how-to-write-and-register-a-handler-to-a-controller) 1. [Creating an Instance of a Controller](#creating-an-instance-of-a-controller) 2. [How to Run Handlers](#how-to-run-handlers) 3. [Different Ways of Interacting with Objects](#different-ways-of-interacting-with-objects) 4. [A Look at Structures Used in Wrangler](#a-look-at-structures-used-in-wrangler)
# How it Works Wrangler provides a code generator that will generate the clientset, informers, listers and additionally generate a controller per resource type. The interface to the controller can be seen in the [Looking at Structures Used in Wrangler](#a-look-at-structures-used-in-wrangler) section.
The controller interface along with other helpful structs, interfaces, and functions are provided by another project [lasso](https://github.com/rancher/lasso). Lasso ties together the aforementioned tools while wrangler leverages them in a user friendly way. To use the controller to run custom code for Kubernetes resource types all one needs to do is register OnChange handlers and run the controller. Also using the controller interface one can access the client and caches through a simple flat API. A typical, non-wrangler Kubernetes application would most likely use an informer for a resource type to add an event handler. Instead, wrangler uses lasso to register each handler which then aggregates the handlers into one function that accepts an object for the controller's resource type and then runs that object through all the handlers. This function is then registered to the Kubernetes informer for that controller's respective resource type. This is done so that an object can run through the handlers in a serialized way. This allows each handler to receive the updated version of the object and avoid many conflicts that would otherwise occur if the handlers were not chained together in this fashion.
## Useful Definitions:
factory
Factories manage controllers. Wrangler generates factories for each API group. Wrangler factories use lasso shared factories for caches and controllers underneath. The lasso factories do most of the heavy lifting but are more resource type agnostic. Wrangler wraps lasso's factories to provide resource type specific clients and controllers. When accessing a wrangler generated controller, a controller for that resource type is requested from a lasso factory. If the controller exists it will be returned. Otherwise, the lasso factory will create it, persist it, and return it. You can consult the [lasso](https://github.com/rancher/lasso) repository for more details on factories.
informers
Broadcasts events for a given resource type and can register handlers for those events.
listers
Sometimes referred to as a cache, uses informers to update a local list of objects for a certain resource type to avoid making requests to the K8s API.
event handlers
Functions that run when a particular event is applied to the resource type the event handler is assigned to.
workqueue
A queue of items to be processed. In this context a queue will usually be a queue of objects of a certain resource type waiting to be processed by all handlers assigned to that resource type.

# How to Use Wrangler Generate controllers for CRDs by using Run() from the controllergen package. This will look like the following: ```golang controllergen.Run(args.Options{ OutputPackage: "github.com/rancher/rancher/pkg/generated", Boilerplate: "scripts/boilerplate.go.txt", Groups: map[string]args.Group{ "management.cattle.io": { PackageName: "management.cattle.io", Types: []interface{}{ // All structs with an embedded ObjectMeta field will be picked up "./pkg/apis/management.cattle.io/v3", // ProjectCatalog and ClusterCatalog are named // explicitly here because they do not have an // ObjectMeta field in their struct. Instead // they embed type v3.Catalog{} which // is a valid object on its own and is generated // above. v3.ProjectCatalog{}, v3.ClusterCatalog{}, }, GenerateTypes: true, }, "ui.cattle.io": { PackageName: "ui.cattle.io", Types: []interface{}{ "./pkg/apis/ui.cattle.io/v1", }, GenerateTypes: true, }, }, }) ``` For the structs to be used when generating controllers they must have the following comments above the structs (note the newline between the comment and struct so it is not rejected by linters): ``` // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object ``` Four types are shown below. This file would be located at `"pkg/apis/management.cattle.io/v3"` relative to the project root directory. The line passing the "./pkg/apis/management.cattle.io/v3" path ensure that the Setting and Catalog controllers are generated. The lines naming the ProjectCatalog and ClusterCatalog structs ensure the respective controllers are generated since neither directly have an ObjectMeta field.: ``` golang import ( "github.com/rancher/norman/types" ) // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type Setting struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Value string `json:"value" norman:"required"` Default string `json:"default" norman:"nocreate,noupdate"` Customized bool `json:"customized" norman:"nocreate,noupdate"` Source string `json:"source" norman:"nocreate,noupdate,options=db|default|env"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ProjectCatalog struct { types.Namespaced Catalog `json:",inline" mapstructure:",squash"` ProjectName string `json:"projectName,omitempty" norman:"type=reference[project]"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ClusterCatalog struct { types.Namespaced Catalog `json:",inline" mapstructure:",squash"` ClusterName string `json:"clusterName,omitempty" norman:"required,type=reference[cluster]"` } // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type Catalog struct { metav1.TypeMeta `json:",inline"` // Standard object’s metadata. More info: // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata metav1.ObjectMeta `json:"metadata,omitempty"` // Specification of the desired behavior of the catalog. More info: // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#spec-and-status Spec CatalogSpec `json:"spec"` Status CatalogStatus `json:"status"` } ``` __Note:__ This is real code taken from [rancher](https://github.com/rancher/rancher) and may not run at the time of reading this. This is meant to provide an example of how one might begin to use wrangler.
### Creating an Instance of a Controller Controllers are categorized by their API group and bundled into a struct called a factory. Functions to create factories are generated by the Run function discussed above. To run one of the functions that creates a factory, import the proper package from the output directory of the generated code. ```golang import ( "github.com/rancher/rancher/pkg/generated/controllers/management.cattle.io" "k8s.io/client-go/rest" ) func createFactory(config *rest.Config) { mgmt, err := management.NewFactoryFromConfig(restConfig) if err != nil { return nil, err } } // Running the functions Management() and V3(), which are the api group and version of the resource types I have generated in this example, is necessary // to instantiate the controller factories for the group and version. User() instantiates the controller for the user resource. This // can be done elsewhere, like when creating a struct but it must be done before the controller is run. Otherwise, the cache will // not work. In this case we are registering a handler so we would have ended up using these methods by necessity, but if we wanted // to access a cache for another resource type in our handler then we also need to make sure it is instantiated in a similar fashion. users := mgmt.Management().V3().User("") ``` ## How to Write and Register a Handler to a Controller Registering a handler means to assign a handler to a specific Kubernetes resource's controller. These handlers will then run when the appropriate event occurs on an object of that controller's resource type. This will be a continuation of our above example: ```golang import ( "context" "github.com/rancher/rancher/pkg/generated/controllers/management.cattle.io" "github.com/rancher/wrangler/v3/pkg/generated/controllers/core" "k8s.io/client-go/rest" ) mgmt, err := management.NewFactoryFromConfig(restConfig) if err != nil { return nil, err } users := mgmt.Management().V3().User("") // passing a namespace here is optional. If an empty string is passed then the client will look at // all configmap objects from all namespaces configmaps := core.Management().Core().Configmaps("examplenamespace") syncHandler := func(id string, obj *v3.User) (*v3.User, error) { if obj != nil { return obj, nil } recordedNote := obj.Annotations != nil && obj.Annotations["wroteanoteaboutuser"] == "true" if recordedNote { // there already is a note, noop return obj, nil } // we are getting the "mainrecord" configmap from the configmap cache. The cache is maintained // locally and can try to fulfill requests without using the k8s api. This is much faster and // efficient, however it does not update immediately so it is possible that if the object // being requested was recently created that the cache will miss and return a not found error. // In this scenario you can either count on the handler reenqueueing and retrying or you can // just use the regular client. record, err := configmaps.Cache().Get("", "mainrecord") if err != nil { return obj, err } record.Data[obj.name] = "recorded" record, err = configmaps.Update(record) if err != nil { return obj, err } // This is done because obj is from the cache that is iterated over to run handlers and perform other tasks. If the subsequent // update fails then we will end up with an object on our cache that does not match the "truth" (how the object is in etcd). obj = obj.DeepCopy() if obj.Annotations == nil { obj.Anotations = make(map[string]string) } obj.Annotations["wroteanoteaboutuser"] = "true" // Here we are using the k8s client embedded onto the users controller to perform an update. This will go to the K8s API. return users.Update(obj) } users.OnChange(context.Background(), "user-example-annotate-note-handler", syncHandler) ``` ### How to Run Handlers Now that we have registered an OnChange handler, we can run it like so: `mgmt.Start(context.Background(), 50)`
### Different Ways of Interacting With Objects In the above example, two clients and one cache are being used to interact with objects. A client can Create, Update, UpdateStatus, Delete, Get, Watch and Patch an object, or List and Watch objects of its respective resource type. A Cache can get an object or list the objects for its respective resource type and will try to get the data locally (from its cache) if possible. The client and cache are the most common ways to interact with an object using wrangler. Another way to interact with objects is to use the Apply client. The apply client works similarly to applying yaml using kubectl. This has benefits such as not assuming the existence of an object like the Update method on a client does. Instead, you can apply a state and the object will be created if it does not exist already or be updated to match the passed desired state if the object does exist already. Apply also allows the use of multiple Owner References in a way unique from the client- if any owner reference is deleted the object will be deleted.
## A Look at Structures Used in Wrangler ```golang type FooController interface { FooClient // OnChange registers a handler that will run whenever an object of the matching resource type is created or updated. This function accepts a sync function specifically generated for the object type and then wraps the function in a function that is compatible with AddGenericHandler. It then uses AddGenericHandler to register the wrapped function. OnChange(ctx context.Context, name string, sync FooHandler) // OnRemove registers a handler that will run whenever an object of the matching resource type is removed. This function accepts a sync function specifically generated for the object type and then wraps the function in a function that is compatible with AddGenericRemoveHandler. It then uses AddGenericRemoveHandler to register the wrapped function. OnRemove(ctx context.Context, name string, sync FooHandler) // Enqueue will rerun all handlers registered to the object's type against the object Enqueue(namespace, name string) // Cache returns a locally maintained cache that can be used for get and list requests Cache() FooCache // Informer returns an informer for the resource type Informer() cache.SharedIndexInformer // GroupVersionKind returns the API group, version, and Kind of the resource type the controller is for GroupVersionKind() schema.GroupVersionKind // AddGenericHandler registers the handler function for the controller AddGenericHandler(ctx context.Context, name string, handler generic.Handler) // AddGenericRemoveHandler registers a handler that will happen when an object of the controller's resource type is removed AddGenericRemoveHandler(ctx context.Context, name string, handler generic.Handler) // Updater returns a function that accepts a runtime.Object and asserts it as the controller's respective resource type struct. It then passes the object to the resource type's client's update function. This is mainly consumed internally by wrangler to implement other functionality. Updater() generic.Updater } type FooClient interface { // Create creates a new instance of resource type in kubernetes Create(*v1alpha1.Foo) (*v1alpha1.Foo, error) // Update updates the given object in kubernetes Update(*v1alpha1.Foo) (*v1alpha1.Foo, error) // Status of type's CRD must be a subresource for this method to be generated. Only updates // status and does not trigger OnChange handlers. UpdateStatus(*v1alpha1.Foo) (*v1alpha1.Foo, error) // Delete deletes the given object in kubernetes Delete(namespace, name string, options *metav1.DeleteOptions) error // Get gets the object of the given name and namespace in kubernetes Get(namespace, name string, options metav1.GetOptions) (*v1alpha1.Foo, error) // List lists all the objects matching the given namespace for the resource type List(namespace string, opts metav1.ListOptions) (*v1alpha1.FooList, error) // Watch returns a channel that will stream objects as they are created, removed, or updated Watch(namespace string, opts metav1.ListOptions) (watch.Interface, error) // Patch accepts a diff that can be applied to an existing object for the client's resource type. Depending on PatchType, which specifies the strategy // to be used when applying the diff, patch can also create a new object. See the following for more information: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/, https://kubernetes.io/docs/reference/using-api/server-side-apply/. Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Foo, err error) } type FooCache interface { // Get gets the object of the given name and namespace from the local cache Get(namespace, name string) (*v1alpha1.Foo, error) // List lists all the objects matching the given namespace for the resource type from the cache List(namespace string, selector labels.Selector) ([]*v1alpha1.Foo, error) // AddIndexer is used to register a function will be used to organize objects in the cache. The indexer will return a string which indicates something about the object. AddIndexer(indexName string, indexer FooIndexer) // GetByIndex will search for objects that match the given key when the named indexer is applied to it GetByIndex(indexName, key string) ([]*v1alpha1.Foo, error) } type FooIndexer func(obj *v1alpha1.Foo) ([]string, error) ``` # Versioning See [VERSION.md](VERSION.md). ================================================ FILE: VERSION.md ================================================ Each wrangler major version supports a range of Kubernetes minor versions. The range supported by each major release line is include below. Wrangler follows the following rules for changes between major/minor/patch: Major Version Increases: - Support for a kubernetes version is explicitly removed (note that this means that wrangler uses a feature that does not work on this version). - A breaking change is made, which is not necessary to resolve a defect. Minor Version Increases: - Support for a kubernetes version is added. - A breaking change in an exported function is made to resolve a defect. Patch Version Increases - A bug was fixed. - A feature was added, in a backwards-compatible way. - A breaking change in an exported function is made to resolve a CVE. Dealing with Kubernetes 1.35 Clients working with versions of Kubernetes before 1.35 might not work with the `main` branch. Use a tag off the `release/v3` branch instead. At a later point there will be a Wrangler Major version `v4`. The current supported release lines are: | Wrangler Branch | Wrangler Major version | Supported Kubernetes Versions | |--------------------------|------------------------------------|------------------------------------------------| | main | v3 | v1.26 - v1.35 | | release/v2 | v2 | v1.23 - v1.26 | | release/v3 | v3 | v1.23 - v1.34 | ================================================ FILE: codegen.go ================================================ //go:generate /bin/rm -rf pkg/generated //go:generate go run pkg/codegen/main.go package main import ( "fmt" ) var ( Version = "v0.0.0-dev" GitCommit = "v0.0.0-dev" ) func main() { fmt.Println("Run go generate") } ================================================ FILE: go.mod ================================================ module github.com/rancher/wrangler/v3 go 1.25.0 require ( github.com/evanphx/json-patch v5.9.11+incompatible github.com/ghodss/yaml v1.0.0 github.com/moby/locker v1.0.1 github.com/rancher/lasso v0.2.7 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 golang.org/x/sync v0.20.0 golang.org/x/text v0.35.0 golang.org/x/tools v0.43.0 k8s.io/api v0.35.0 k8s.io/apiextensions-apiserver v0.35.0 k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 k8s.io/code-generator v0.35.0 k8s.io/gengo v0.0.0-20250130153323-76c5745d3511 k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b k8s.io/kube-aggregator v0.35.0 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 sigs.k8s.io/cli-utils v0.37.2 ) require ( cel.dev/expr v0.24.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/cobra v1.10.0 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.35.0 // indirect k8s.io/component-base v0.35.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rancher/lasso v0.2.7 h1:N56Pm8KJSk7gRVYILSGb35QBfjcDDeGFMfwUCivsbeg= github.com/rancher/lasso v0.2.7/go.mod h1:L3ol8PdO21KoMhNa3RWjpR3ZBnE70JCAod1nJuOvT1E= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/gengo v0.0.0-20250130153323-76c5745d3511 h1:4eL6zr5VCj71nu2nOuQ6j6m/kqh5WueXBN8daZkNe90= k8s.io/gengo v0.0.0-20250130153323-76c5745d3511/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-aggregator v0.35.0 h1:FBtbuRFA7Ohe2QKirFZcJf8rgimC8oSaNiCi4pdU5xw= k8s.io/kube-aggregator v0.35.0/go.mod h1:vKBRpQUfDryb7udwUwF3eCSvv3AJNgHtL4PGl6PqAg8= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: pkg/apply/apply.go ================================================ package apply import ( "context" "fmt" "sync" "github.com/rancher/wrangler/v3/pkg/apply/injectors" "github.com/rancher/wrangler/v3/pkg/objectset" "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/dynamic" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" ) const ( defaultNamespace = "default" ) type Patcher func(namespace, name string, pt types.PatchType, data []byte) (runtime.Object, error) // Reconciler return false if it did not handle this object type Reconciler func(oldObj runtime.Object, newObj runtime.Object) (bool, error) type ClientFactory func(gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error) type InformerFactory interface { Get(gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) (cache.SharedIndexInformer, error) } type InformerGetter interface { Informer() cache.SharedIndexInformer GroupVersionKind() schema.GroupVersionKind } type PatchByGVK map[schema.GroupVersionKind]map[objectset.ObjectKey]string func (p PatchByGVK) Add(gvk schema.GroupVersionKind, namespace, name, patch string) { d, ok := p[gvk] if !ok { d = map[objectset.ObjectKey]string{} p[gvk] = d } d[objectset.ObjectKey{ Name: name, Namespace: namespace, }] = patch } type Plan struct { Create objectset.ObjectKeyByGVK Delete objectset.ObjectKeyByGVK Update PatchByGVK Objects []runtime.Object } type Apply interface { Apply(set *objectset.ObjectSet) error ApplyObjects(objs ...runtime.Object) error WithContext(ctx context.Context) Apply WithCacheTypes(igs ...InformerGetter) Apply WithCacheTypeFactory(factory InformerFactory) Apply WithSetID(id string) Apply WithOwner(obj runtime.Object) Apply WithOwnerKey(key string, gvk schema.GroupVersionKind) Apply WithInjector(injs ...injectors.ConfigInjector) Apply WithInjectorName(injs ...string) Apply WithPatcher(gvk schema.GroupVersionKind, patchers Patcher) Apply WithReconciler(gvk schema.GroupVersionKind, reconciler Reconciler) Apply WithStrictCaching() Apply WithDynamicLookup() Apply WithRestrictClusterScoped() Apply WithDefaultNamespace(ns string) Apply WithListerNamespace(ns string) Apply WithRateLimiting(ratelimitingQPS float32) Apply WithNoDelete() Apply WithNoDeleteGVK(gvks ...schema.GroupVersionKind) Apply WithGVK(gvks ...schema.GroupVersionKind) Apply WithSetOwnerReference(controller, block bool) Apply WithIgnorePreviousApplied() Apply WithDiffPatch(gvk schema.GroupVersionKind, namespace, name string, patch []byte) Apply FindOwner(obj runtime.Object) (runtime.Object, error) PurgeOrphan(obj runtime.Object) error DryRun(objs ...runtime.Object) (Plan, error) } func NewForConfig(cfg *rest.Config) (Apply, error) { discovery, err := discovery.NewDiscoveryClientForConfig(cfg) if err != nil { return nil, err } return New(discovery, NewClientFactory(cfg)), nil } func New(discovery discovery.DiscoveryInterface, cf ClientFactory, igs ...InformerGetter) Apply { a := &apply{ clients: &clients{ clientFactory: cf, discovery: discovery, namespaced: map[schema.GroupVersionKind]bool{}, gvkToGVR: map[schema.GroupVersionKind]schema.GroupVersionResource{}, clients: map[schema.GroupVersionKind]dynamic.NamespaceableResourceInterface{}, }, informers: map[schema.GroupVersionKind]cache.SharedIndexInformer{}, } for _, ig := range igs { a.informers[ig.GroupVersionKind()] = ig.Informer() } return a } type apply struct { clients *clients informers map[schema.GroupVersionKind]cache.SharedIndexInformer } type clients struct { sync.Mutex clientFactory ClientFactory discovery discovery.DiscoveryInterface namespaced map[schema.GroupVersionKind]bool gvkToGVR map[schema.GroupVersionKind]schema.GroupVersionResource clients map[schema.GroupVersionKind]dynamic.NamespaceableResourceInterface } func (c *clients) IsNamespaced(gvk schema.GroupVersionKind) (bool, error) { c.Lock() ok, exists := c.namespaced[gvk] c.Unlock() if exists { return ok, nil } _, err := c.client(gvk) if err != nil { return false, err } c.Lock() defer c.Unlock() return c.namespaced[gvk], nil } func (c *clients) gvr(gvk schema.GroupVersionKind) schema.GroupVersionResource { c.Lock() defer c.Unlock() return c.gvkToGVR[gvk] } func (c *clients) client(gvk schema.GroupVersionKind) (dynamic.NamespaceableResourceInterface, error) { c.Lock() defer c.Unlock() if client, ok := c.clients[gvk]; ok { return client, nil } resources, err := c.discovery.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { return nil, err } for _, resource := range resources.APIResources { if resource.Kind != gvk.Kind { continue } client, err := c.clientFactory(gvk.GroupVersion().WithResource(resource.Name)) if err != nil { return nil, err } c.namespaced[gvk] = resource.Namespaced c.clients[gvk] = client c.gvkToGVR[gvk] = gvk.GroupVersion().WithResource(resource.Name) return client, nil } return nil, fmt.Errorf("failed to discover client for %s", gvk) } func (a *apply) newDesiredSet() desiredSet { return desiredSet{ a: a, defaultNamespace: defaultNamespace, ctx: context.Background(), ratelimitingQPS: 1, reconcilers: defaultReconcilers, strictCaching: true, } } func (a *apply) DryRun(objs ...runtime.Object) (Plan, error) { return a.newDesiredSet().DryRun(objs...) } func (a *apply) Apply(set *objectset.ObjectSet) error { return a.newDesiredSet().Apply(set) } func (a *apply) ApplyObjects(objs ...runtime.Object) error { os := objectset.NewObjectSet() os.Add(objs...) return a.newDesiredSet().Apply(os) } func (a *apply) WithSetID(id string) Apply { return a.newDesiredSet().WithSetID(id) } func (a *apply) WithOwner(obj runtime.Object) Apply { return a.newDesiredSet().WithOwner(obj) } func (a *apply) WithOwnerKey(key string, gvk schema.GroupVersionKind) Apply { return a.newDesiredSet().WithOwnerKey(key, gvk) } func (a *apply) WithInjector(injs ...injectors.ConfigInjector) Apply { return a.newDesiredSet().WithInjector(injs...) } func (a *apply) WithInjectorName(injs ...string) Apply { return a.newDesiredSet().WithInjectorName(injs...) } func (a *apply) WithCacheTypes(igs ...InformerGetter) Apply { return a.newDesiredSet().WithCacheTypes(igs...) } func (a *apply) WithCacheTypeFactory(factory InformerFactory) Apply { return a.newDesiredSet().WithCacheTypeFactory(factory) } func (a *apply) WithGVK(gvks ...schema.GroupVersionKind) Apply { return a.newDesiredSet().WithGVK(gvks...) } func (a *apply) WithPatcher(gvk schema.GroupVersionKind, patcher Patcher) Apply { return a.newDesiredSet().WithPatcher(gvk, patcher) } func (a *apply) WithReconciler(gvk schema.GroupVersionKind, reconciler Reconciler) Apply { return a.newDesiredSet().WithReconciler(gvk, reconciler) } func (a *apply) WithStrictCaching() Apply { return a.newDesiredSet().WithStrictCaching() } func (a *apply) WithDynamicLookup() Apply { return a.newDesiredSet().WithDynamicLookup() } func (a *apply) WithRestrictClusterScoped() Apply { return a.newDesiredSet().WithRestrictClusterScoped() } func (a *apply) WithDefaultNamespace(ns string) Apply { return a.newDesiredSet().WithDefaultNamespace(ns) } func (a *apply) WithListerNamespace(ns string) Apply { return a.newDesiredSet().WithListerNamespace(ns) } func (a *apply) WithRateLimiting(ratelimitingQPS float32) Apply { return a.newDesiredSet().WithRateLimiting(ratelimitingQPS) } func (a *apply) WithNoDelete() Apply { return a.newDesiredSet().WithNoDelete() } func (a *apply) WithNoDeleteGVK(gvks ...schema.GroupVersionKind) Apply { return a.newDesiredSet().WithNoDeleteGVK(gvks...) } func (a *apply) WithSetOwnerReference(controller, block bool) Apply { return a.newDesiredSet().WithSetOwnerReference(controller, block) } func (a *apply) WithContext(ctx context.Context) Apply { return a.newDesiredSet().WithContext(ctx) } func (a *apply) WithIgnorePreviousApplied() Apply { return a.newDesiredSet().WithIgnorePreviousApplied() } func (a *apply) FindOwner(obj runtime.Object) (runtime.Object, error) { return a.newDesiredSet().FindOwner(obj) } func (a *apply) PurgeOrphan(obj runtime.Object) error { return a.newDesiredSet().PurgeOrphan(obj) } func (a *apply) WithDiffPatch(gvk schema.GroupVersionKind, namespace, name string, patch []byte) Apply { return a.newDesiredSet().WithDiffPatch(gvk, namespace, name, patch) } ================================================ FILE: pkg/apply/client_factory.go ================================================ package apply import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" ) func NewClientFactory(config *rest.Config) ClientFactory { return func(gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error) { client, err := dynamic.NewForConfig(config) if err != nil { return nil, err } return client.Resource(gvr), nil } } ================================================ FILE: pkg/apply/desiredset.go ================================================ package apply import ( "context" "errors" "fmt" "github.com/rancher/wrangler/v3/pkg/apply/injectors" "github.com/rancher/wrangler/v3/pkg/kv" "github.com/rancher/wrangler/v3/pkg/merr" "github.com/rancher/wrangler/v3/pkg/objectset" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/cache" ) // Indexer name added for cached types const byHash = "wrangler.byObjectSetHash" type patchKey struct { schema.GroupVersionKind objectset.ObjectKey } type desiredSet struct { a *apply ctx context.Context defaultNamespace string listerNamespace string ignorePreviousApplied bool setOwnerReference bool ownerReferenceController bool ownerReferenceBlock bool strictCaching bool restrictClusterScoped bool pruneTypes map[schema.GroupVersionKind]cache.SharedIndexInformer patchers map[schema.GroupVersionKind]Patcher reconcilers map[schema.GroupVersionKind]Reconciler diffPatches map[patchKey][][]byte informerFactory InformerFactory remove bool noDelete bool noDeleteGVK map[schema.GroupVersionKind]struct{} setID string objs *objectset.ObjectSet owner runtime.Object injectors []injectors.ConfigInjector ratelimitingQPS float32 injectorNames []string errs []error createPlan bool plan Plan } func (o *desiredSet) err(err error) error { o.errs = append(o.errs, err) return o.Err() } func (o desiredSet) Err() error { return merr.NewErrors(append(o.errs, o.objs.Err())...) } func (o desiredSet) DryRun(objs ...runtime.Object) (Plan, error) { o.objs = objectset.NewObjectSet() o.objs.Add(objs...) return o.dryRun() } func (o desiredSet) Apply(set *objectset.ObjectSet) error { if set == nil { set = objectset.NewObjectSet() } o.objs = set return o.apply() } func (o desiredSet) ApplyObjects(objs ...runtime.Object) error { os := objectset.NewObjectSet() os.Add(objs...) return o.Apply(os) } func (o desiredSet) WithDiffPatch(gvk schema.GroupVersionKind, namespace, name string, patch []byte) Apply { patches := map[patchKey][][]byte{} for k, v := range o.diffPatches { patches[k] = v } key := patchKey{ GroupVersionKind: gvk, ObjectKey: objectset.ObjectKey{ Name: name, Namespace: namespace, }, } patches[key] = append(patches[key], patch) o.diffPatches = patches return o } // WithGVK uses a known listing of existing gvks to modify the the prune types to allow for deletion of objects func (o desiredSet) WithGVK(gvks ...schema.GroupVersionKind) Apply { pruneTypes := make(map[schema.GroupVersionKind]cache.SharedIndexInformer, len(gvks)) for k, v := range o.pruneTypes { pruneTypes[k] = v } for _, gvk := range gvks { pruneTypes[gvk] = nil } o.pruneTypes = pruneTypes return o } func (o desiredSet) WithSetID(id string) Apply { o.setID = id return o } func (o desiredSet) WithOwnerKey(key string, gvk schema.GroupVersionKind) Apply { obj := &v1.PartialObjectMetadata{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(gvk) o.owner = obj return o } func (o desiredSet) WithOwner(obj runtime.Object) Apply { o.owner = obj return o } func (o desiredSet) WithSetOwnerReference(controller, block bool) Apply { o.setOwnerReference = true o.ownerReferenceController = controller o.ownerReferenceBlock = block return o } func (o desiredSet) WithInjector(injs ...injectors.ConfigInjector) Apply { o.injectors = append(o.injectors, injs...) return o } func (o desiredSet) WithInjectorName(injs ...string) Apply { o.injectorNames = append(o.injectorNames, injs...) return o } func (o desiredSet) WithCacheTypeFactory(factory InformerFactory) Apply { o.informerFactory = factory return o } func (o desiredSet) WithIgnorePreviousApplied() Apply { o.ignorePreviousApplied = true return o } func (o desiredSet) WithCacheTypes(igs ...InformerGetter) Apply { pruneTypes := make(map[schema.GroupVersionKind]cache.SharedIndexInformer, len(igs)) for k, v := range o.pruneTypes { pruneTypes[k] = v } for _, ig := range igs { informer := ig.Informer() if err := addIndexerByHash(informer.GetIndexer()); err != nil { // Ignore repeatedly adding the same indexer for different types if !errors.Is(err, errIndexerAlreadyExists) { logrus.Warnf("Problem adding hash indexer to informer [%s]: %v", ig.GroupVersionKind().Kind, err) } } pruneTypes[ig.GroupVersionKind()] = informer } o.pruneTypes = pruneTypes return o } // addIndexerByHash an Informer to index objects by the hash annotation value func addIndexerByHash(indexer cache.Indexer) error { if _, alreadyAdded := indexer.GetIndexers()[byHash]; alreadyAdded { return fmt.Errorf("adding indexer %q: %w", byHash, errIndexerAlreadyExists) } return indexer.AddIndexers(map[string]cache.IndexFunc{ byHash: func(obj interface{}) ([]string, error) { metadata, err := meta.Accessor(obj) if err != nil { return nil, err } labels := metadata.GetLabels() if labels == nil || labels[LabelHash] == "" { return nil, nil } return []string{labels[LabelHash]}, nil }, }) } func (o desiredSet) WithPatcher(gvk schema.GroupVersionKind, patcher Patcher) Apply { patchers := map[schema.GroupVersionKind]Patcher{} for k, v := range o.patchers { patchers[k] = v } patchers[gvk] = patcher o.patchers = patchers return o } func (o desiredSet) WithReconciler(gvk schema.GroupVersionKind, reconciler Reconciler) Apply { reconcilers := map[schema.GroupVersionKind]Reconciler{} for k, v := range o.reconcilers { reconcilers[k] = v } reconcilers[gvk] = reconciler o.reconcilers = reconcilers return o } func (o desiredSet) WithStrictCaching() Apply { o.strictCaching = true return o } func (o desiredSet) WithDynamicLookup() Apply { o.strictCaching = false return o } func (o desiredSet) WithRestrictClusterScoped() Apply { o.restrictClusterScoped = true return o } func (o desiredSet) WithDefaultNamespace(ns string) Apply { if ns == "" { o.defaultNamespace = defaultNamespace } else { o.defaultNamespace = ns } return o } func (o desiredSet) WithListerNamespace(ns string) Apply { o.listerNamespace = ns return o } func (o desiredSet) WithRateLimiting(ratelimitingQPS float32) Apply { o.ratelimitingQPS = ratelimitingQPS return o } func (o desiredSet) WithNoDelete() Apply { o.noDelete = true return o } func (o desiredSet) WithNoDeleteGVK(gvks ...schema.GroupVersionKind) Apply { if o.noDeleteGVK == nil { o.noDeleteGVK = make(map[schema.GroupVersionKind]struct{}) } for _, curr := range gvks { o.noDeleteGVK[curr] = struct{}{} } return o } func (o desiredSet) WithContext(ctx context.Context) Apply { o.ctx = ctx return o } ================================================ FILE: pkg/apply/desiredset_apply.go ================================================ package apply import ( "crypto/sha1" "encoding/hex" "errors" "fmt" "sync" "time" "github.com/sirupsen/logrus" gvk2 "github.com/rancher/wrangler/v3/pkg/gvk" "github.com/rancher/wrangler/v3/pkg/apply/injectors" "github.com/rancher/wrangler/v3/pkg/objectset" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/util/flowcontrol" ) const ( LabelID = "objectset.rio.cattle.io/id" LabelGVK = "objectset.rio.cattle.io/owner-gvk" LabelName = "objectset.rio.cattle.io/owner-name" LabelNamespace = "objectset.rio.cattle.io/owner-namespace" LabelHash = "objectset.rio.cattle.io/hash" LabelPrefix = "objectset.rio.cattle.io/" LabelPrune = "objectset.rio.cattle.io/prune" ) var ( hashOrder = []string{ LabelID, LabelGVK, LabelName, LabelNamespace, } rls = map[string]flowcontrol.RateLimiter{} rlsLock sync.Mutex errIndexerAlreadyExists = errors.New("an indexer with the same already exists") ) func (o *desiredSet) getRateLimit(labelHash string) flowcontrol.RateLimiter { var rl flowcontrol.RateLimiter rlsLock.Lock() defer rlsLock.Unlock() if o.remove { delete(rls, labelHash) } else { rl = rls[labelHash] if rl == nil { rl = flowcontrol.NewTokenBucketRateLimiter(o.ratelimitingQPS, 10) rls[labelHash] = rl } } return rl } func (o *desiredSet) dryRun() (Plan, error) { o.createPlan = true o.plan.Create = objectset.ObjectKeyByGVK{} o.plan.Update = PatchByGVK{} o.plan.Delete = objectset.ObjectKeyByGVK{} err := o.apply() return o.plan, err } func (o *desiredSet) apply() error { if o.objs == nil || o.objs.Len() == 0 { o.remove = true } if err := o.Err(); err != nil { return err } labelSet, annotationSet, err := GetLabelsAndAnnotations(o.setID, o.owner) if err != nil { return o.err(err) } rl := o.getRateLimit(labelSet[LabelHash]) if rl != nil { t := time.Now() rl.Accept() if d := time.Now().Sub(t); d.Seconds() > 1 { logrus.Infof("rate limited %s(%s) %s", o.setID, labelSet, d) } } objList, err := o.injectLabelsAndAnnotations(labelSet, annotationSet) if err != nil { return o.err(err) } objList, err = o.runInjectors(objList) if err != nil { return o.err(err) } objs := o.collect(objList) debugID := o.debugID() sel, err := GetSelector(labelSet) if err != nil { return o.err(err) } for _, gvk := range o.objs.GVKOrder(o.knownGVK()...) { o.process(debugID, sel, gvk, objs[gvk]) } return o.Err() } func (o *desiredSet) knownGVK() (ret []schema.GroupVersionKind) { for k := range o.pruneTypes { ret = append(ret, k) } return } func (o *desiredSet) debugID() string { if o.owner == nil { return o.setID } metadata, err := meta.Accessor(o.owner) if err != nil { return o.setID } return fmt.Sprintf("%s %s", o.setID, objectset.ObjectKey{ Namespace: metadata.GetNamespace(), Name: metadata.GetName(), }) } func (o *desiredSet) collect(objList []runtime.Object) objectset.ObjectByGVK { result := objectset.ObjectByGVK{} for _, obj := range objList { _, _ = result.Add(obj) } return result } func (o *desiredSet) runInjectors(objList []runtime.Object) ([]runtime.Object, error) { var err error for _, inj := range o.injectors { if inj == nil { continue } objList, err = inj(objList) if err != nil { return nil, err } } for _, name := range o.injectorNames { inj := injectors.Get(name) if inj == nil { continue } objList, err = inj(objList) if err != nil { return nil, err } } return objList, nil } // GetSelectorFromOwner returns the label selector for the owner object which is useful // to list the dependents func GetSelectorFromOwner(setID string, owner runtime.Object) (labels.Selector, error) { // Build the labels, we want the hash label for the lister ownerLabel, _, err := GetLabelsAndAnnotations(setID, owner) if err != nil { return nil, err } return GetSelector(ownerLabel) } func GetSelector(labelSet map[string]string) (labels.Selector, error) { req, err := labels.NewRequirement(LabelHash, selection.Equals, []string{labelSet[LabelHash]}) if err != nil { return nil, err } return labels.NewSelector().Add(*req), nil } func GetLabelsAndAnnotations(setID string, owner runtime.Object) (map[string]string, map[string]string, error) { if setID == "" && owner == nil { return nil, nil, fmt.Errorf("set ID or owner must be set") } annotations := map[string]string{ LabelID: setID, } if owner != nil { gvk, err := gvk2.Get(owner) if err != nil { return nil, nil, err } annotations[LabelGVK] = gvk.String() metadata, err := meta.Accessor(owner) if err != nil { return nil, nil, fmt.Errorf("failed to get metadata for %s", gvk) } annotations[LabelName] = metadata.GetName() annotations[LabelNamespace] = metadata.GetNamespace() } labels := map[string]string{ LabelHash: objectSetHash(annotations), } return labels, annotations, nil } func (o *desiredSet) injectLabelsAndAnnotations(labels, annotations map[string]string) ([]runtime.Object, error) { var result []runtime.Object for _, objMap := range o.objs.ObjectsByGVK() { for key, obj := range objMap { obj = obj.DeepCopyObject() meta, err := meta.Accessor(obj) if err != nil { return nil, fmt.Errorf("failed to get metadata for %s: %w", key, err) } setLabels(meta, labels) setAnnotations(meta, annotations) result = append(result, obj) } } return result, nil } func setAnnotations(meta metav1.Object, annotations map[string]string) { objAnn := meta.GetAnnotations() if objAnn == nil { objAnn = map[string]string{} } delete(objAnn, LabelApplied) for k, v := range annotations { objAnn[k] = v } meta.SetAnnotations(objAnn) } func setLabels(meta metav1.Object, labels map[string]string) { objLabels := meta.GetLabels() if objLabels == nil { objLabels = map[string]string{} } for k, v := range labels { objLabels[k] = v } meta.SetLabels(objLabels) } func objectSetHash(labels map[string]string) string { dig := sha1.New() for _, key := range hashOrder { dig.Write([]byte(labels[key])) } return hex.EncodeToString(dig.Sum(nil)) } ================================================ FILE: pkg/apply/desiredset_compare.go ================================================ package apply import ( "bytes" "compress/gzip" "encoding/base64" "fmt" "io" "io/ioutil" "strings" "sync" jsonpatch "github.com/evanphx/json-patch" data2 "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/objectset" patch2 "github.com/rancher/wrangler/v3/pkg/patch" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/meta" v1 "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/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/jsonmergepatch" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/dynamic" ) const ( LabelApplied = "objectset.rio.cattle.io/applied" ) var ( knownListKeys = map[string]bool{ "apiVersion": true, "containerPort": true, "devicePath": true, "ip": true, "kind": true, "mountPath": true, "name": true, "port": true, "topologyKey": true, "type": true, } // Pools used by gzip writers buffersPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } gzipWritersPool = sync.Pool{ New: func() interface{} { // Initialize a new writer with discarding destination // It should be reconfigured prior usage with an actual buffer using Reset() return gzip.NewWriter(io.Discard) }, } ) func prepareObjectForCreate(gvk schema.GroupVersionKind, obj runtime.Object) (runtime.Object, error) { serialized, err := serializeApplied(obj) if err != nil { return nil, err } obj = obj.DeepCopyObject() m, err := meta.Accessor(obj) if err != nil { return nil, err } annotations := m.GetAnnotations() if annotations == nil { annotations = map[string]string{} } annotations[LabelApplied] = appliedToAnnotation(serialized) m.SetAnnotations(annotations) typed, err := meta.TypeAccessor(obj) if err != nil { return nil, err } apiVersion, kind := gvk.ToAPIVersionAndKind() typed.SetAPIVersion(apiVersion) typed.SetKind(kind) return obj, nil } func originalAndModified(gvk schema.GroupVersionKind, oldMetadata v1.Object, newObject runtime.Object) ([]byte, []byte, error) { original, err := getOriginalBytes(gvk, oldMetadata) if err != nil { return nil, nil, err } newObject, err = prepareObjectForCreate(gvk, newObject) if err != nil { return nil, nil, err } modified, err := json.Marshal(newObject) return original, modified, err } func emptyMaps(data map[string]interface{}, keys ...string) bool { for _, key := range append(keys, "__invalid_key__") { if len(data) == 0 { // map is empty so all children are empty too return true } else if len(data) > 1 { // map has more than one key so not empty return false } value, ok := data[key] if !ok { // map has one key but not what we are expecting so not considered empty return false } data = convert.ToMapInterface(value) } return true } func sanitizePatch(patch []byte, removeObjectSetAnnotation bool) ([]byte, error) { mod := false data := map[string]interface{}{} err := json.Unmarshal(patch, &data) if err != nil { return nil, err } if _, ok := data["kind"]; ok { mod = true delete(data, "kind") } if _, ok := data["apiVersion"]; ok { mod = true delete(data, "apiVersion") } if _, ok := data["status"]; ok { mod = true delete(data, "status") } if deleted := removeCreationTimestamp(data); deleted { mod = true } if removeObjectSetAnnotation { metadata := convert.ToMapInterface(data2.GetValueN(data, "metadata")) annotations := convert.ToMapInterface(data2.GetValueN(data, "metadata", "annotations")) for k := range annotations { if strings.HasPrefix(k, LabelPrefix) { mod = true delete(annotations, k) } } if mod && len(annotations) == 0 { delete(metadata, "annotations") if len(metadata) == 0 { delete(data, "metadata") } } } if emptyMaps(data, "metadata", "annotations") { return []byte("{}"), nil } if !mod { return patch, nil } return json.Marshal(data) } func applyPatch(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patcher, debugID string, ignoreOriginal bool, oldObject, newObject runtime.Object, diffPatches [][]byte) (bool, error) { oldMetadata, err := meta.Accessor(oldObject) if err != nil { return false, err } original, modified, err := originalAndModified(gvk, oldMetadata, newObject) if err != nil { return false, err } if ignoreOriginal { original = nil } current, err := json.Marshal(oldObject) if err != nil { return false, err } patchType, patch, err := doPatch(gvk, original, modified, current, diffPatches) if err != nil { return false, fmt.Errorf("patch generation: %w", err) } if string(patch) == "{}" { return false, nil } patch, err = sanitizePatch(patch, false) if err != nil { return false, err } if string(patch) == "{}" { return false, nil } logrus.Debugf("DesiredSet - Patch %s %s/%s for %s -- [PATCH:%s, ORIGINAL:%s, MODIFIED:%s, CURRENT:%s]", gvk, oldMetadata.GetNamespace(), oldMetadata.GetName(), debugID, patch, original, modified, current) if reconciler != nil { newObject, err := prepareObjectForCreate(gvk, newObject) if err != nil { return false, err } originalObject, err := getOriginalObject(gvk, oldMetadata) if err != nil { return false, err } if originalObject == nil { originalObject = oldObject } handled, err := reconciler(originalObject, newObject) if err != nil { return false, err } if handled { return true, nil } } logrus.Debugf("DesiredSet - Updated %s %s/%s for %s -- %s %s", gvk, oldMetadata.GetNamespace(), oldMetadata.GetName(), debugID, patchType, patch) _, err = patcher(oldMetadata.GetNamespace(), oldMetadata.GetName(), patchType, patch) return true, err } func (o *desiredSet) compareObjects(gvk schema.GroupVersionKind, reconciler Reconciler, patcher Patcher, client dynamic.NamespaceableResourceInterface, debugID string, oldObject, newObject runtime.Object, force bool) error { oldMetadata, err := meta.Accessor(oldObject) if err != nil { return err } if o.createPlan { o.plan.Objects = append(o.plan.Objects, oldObject) } diffPatches := o.diffPatches[patchKey{ GroupVersionKind: gvk, ObjectKey: objectset.ObjectKey{ Namespace: oldMetadata.GetNamespace(), Name: oldMetadata.GetName(), }, }] diffPatches = append(diffPatches, o.diffPatches[patchKey{ GroupVersionKind: gvk, }]...) if ran, err := applyPatch(gvk, reconciler, patcher, debugID, o.ignorePreviousApplied, oldObject, newObject, diffPatches); err != nil { return err } else if !ran { logrus.Debugf("DesiredSet - No change(2) %s %s/%s for %s", gvk, oldMetadata.GetNamespace(), oldMetadata.GetName(), debugID) } return nil } func removeCreationTimestamp(data map[string]interface{}) bool { metadata, ok := data["metadata"] if !ok { return false } data = convert.ToMapInterface(metadata) if _, ok := data["creationTimestamp"]; ok { delete(data, "creationTimestamp") return true } return false } func getOriginalObject(gvk schema.GroupVersionKind, obj v1.Object) (runtime.Object, error) { original := appliedFromAnnotation(obj.GetAnnotations()[LabelApplied]) if len(original) == 0 { return nil, nil } mapObj := map[string]interface{}{} err := json.Unmarshal(original, &mapObj) if err != nil { return nil, err } removeCreationTimestamp(mapObj) return prepareObjectForCreate(gvk, &unstructured.Unstructured{ Object: mapObj, }) } func getOriginalBytes(gvk schema.GroupVersionKind, obj v1.Object) ([]byte, error) { objCopy, err := getOriginalObject(gvk, obj) if err != nil { return nil, err } if objCopy == nil { return []byte("{}"), nil } return json.Marshal(objCopy) } func appliedFromAnnotation(str string) []byte { if len(str) == 0 || str[0] == '{' { return []byte(str) } b, err := base64.RawStdEncoding.DecodeString(str) if err != nil { return nil } r, err := gzip.NewReader(bytes.NewBuffer(b)) if err != nil { return nil } b, err = ioutil.ReadAll(r) if err != nil { return nil } return b } func pruneList(data []interface{}) []interface{} { result := make([]interface{}, 0, len(data)) for _, v := range data { switch typed := v.(type) { case map[string]interface{}: result = append(result, pruneValues(typed, true)) case []interface{}: result = append(result, pruneList(typed)) default: result = append(result, v) } } return result } func pruneValues(data map[string]interface{}, isList bool) map[string]interface{} { result := map[string]interface{}{} for k, v := range data { switch typed := v.(type) { case map[string]interface{}: result[k] = pruneValues(typed, false) case []interface{}: result[k] = pruneList(typed) default: if isList && knownListKeys[k] { result[k] = v } else { switch x := v.(type) { case string: if len(x) > 64 { result[k] = x[:64] } else { result[k] = v } case []byte: result[k] = nil default: result[k] = v } } } } return result } func serializeApplied(obj runtime.Object) ([]byte, error) { data, err := convert.EncodeToMap(obj) if err != nil { return nil, err } data = pruneValues(data, false) return json.Marshal(data) } func appliedToAnnotation(b []byte) string { return compressAndEncode(b) } func compressAndEncode(b []byte) string { buf := buffersPool.Get().(*bytes.Buffer) buf.Reset() defer buffersPool.Put(buf) w := gzipWritersPool.Get().(*gzip.Writer) w.Reset(buf) defer gzipWritersPool.Put(w) if _, err := w.Write(b); err != nil { return string(b) } if err := w.Close(); err != nil { return string(b) } return base64.RawStdEncoding.EncodeToString(buf.Bytes()) } func stripIgnores(original, modified, current []byte, patches [][]byte) ([]byte, []byte, []byte, error) { for _, patch := range patches { patch, err := jsonpatch.DecodePatch(patch) if err != nil { return nil, nil, nil, err } if len(original) > 0 { b, err := patch.Apply(original) if err == nil { original = b } } b, err := patch.Apply(modified) if err == nil { modified = b } b, err = patch.Apply(current) if err == nil { current = b } } return original, modified, current, nil } // doPatch is adapted from "kubectl apply" func doPatch(gvk schema.GroupVersionKind, original, modified, current []byte, diffPatch [][]byte) (types.PatchType, []byte, error) { var ( patchType types.PatchType patch []byte ) original, modified, current, err := stripIgnores(original, modified, current, diffPatch) if err != nil { return patchType, nil, err } patchType, lookupPatchMeta, err := patch2.GetMergeStyle(gvk) if err != nil { return patchType, nil, err } if patchType == types.StrategicMergePatchType { patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, true) } else { patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current) } if err != nil { logrus.Errorf("Failed to calcuated patch: %v", err) } return patchType, patch, err } ================================================ FILE: pkg/apply/desiredset_compare_test.go ================================================ package apply import ( "bytes" "testing" ) func TestCompressAndEncode(t *testing.T) { testCases := []struct { name string input []byte expected string }{ { name: "Empty string", input: []byte(""), expected: "H4sIAAAAAAAA/wEAAP//AAAAAAAAAAA", }, { name: "Short string", input: []byte("hello world"), expected: "H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA", }, { name: "JSON payload", input: []byte(`{"id": 123, "status": "active", "message": "hello"}`), expected: "H4sIAAAAAAAA/6pWykxRslIwNDLWUVAqLkksKS1WslJQSkwuySxLVdJRUMpNLS5OTE8FCWak5uTkK9UCAgAA//9XG2xwMwAAAA", }, { name: "Longer repeating string", input: bytes.Repeat([]byte("test data "), 10), expected: "H4sIAAAAAAAA/ypJLS5RSEksSVSgHQsQAAD//02/IfBkAAAA", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Compare the result against our hardcoded golden value if got, want := compressAndEncode(tc.input), tc.expected; got != want { t.Errorf("got %q, want %q", got, want) } }) } } ================================================ FILE: pkg/apply/desiredset_crud.go ================================================ package apply import ( "bytes" v1 "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/util/json" "k8s.io/client-go/dynamic" ) var ( deletePolicy = v1.DeletePropagationBackground ) func (o *desiredSet) toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) { unstruct, ok := obj.(*unstructured.Unstructured) if ok { return unstruct, nil } buf := &bytes.Buffer{} if err := json.NewEncoder(buf).Encode(obj); err != nil { return nil, err } unstruct = &unstructured.Unstructured{ Object: map[string]interface{}{}, } return unstruct, json.Unmarshal(buf.Bytes(), &unstruct.Object) } func (o *desiredSet) create(nsed bool, namespace string, client dynamic.NamespaceableResourceInterface, obj runtime.Object) (runtime.Object, error) { unstr, err := o.toUnstructured(obj) if err != nil { return nil, err } if nsed { return client.Namespace(namespace).Create(o.ctx, unstr, v1.CreateOptions{}) } return client.Create(o.ctx, unstr, v1.CreateOptions{}) } func (o *desiredSet) get(nsed bool, namespace, name string, client dynamic.NamespaceableResourceInterface) (runtime.Object, error) { if nsed { return client.Namespace(namespace).Get(o.ctx, name, v1.GetOptions{}) } return client.Get(o.ctx, name, v1.GetOptions{}) } func (o *desiredSet) delete(nsed bool, namespace, name string, client dynamic.NamespaceableResourceInterface, force bool, gvk schema.GroupVersionKind) error { if !force { if o.noDelete { return nil } if _, ok := o.noDeleteGVK[gvk]; ok { return nil } } opts := v1.DeleteOptions{ PropagationPolicy: &deletePolicy, } if nsed { return client.Namespace(namespace).Delete(o.ctx, name, opts) } return client.Delete(o.ctx, name, opts) } ================================================ FILE: pkg/apply/desiredset_owner.go ================================================ package apply import ( "errors" "fmt" "strings" "github.com/rancher/wrangler/v3/pkg/gvk" "github.com/rancher/wrangler/v3/pkg/kv" namer "github.com/rancher/wrangler/v3/pkg/name" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/cache" ) var ( ErrOwnerNotFound = errors.New("owner not found") ErrNoInformerFound = errors.New("informer not found") ) func notFound(name string, gvk schema.GroupVersionKind) error { // this is not proper, but does it really matter that much? If you find this // line while researching a bug, then the answer is probably yes. resource := namer.GuessPluralName(strings.ToLower(gvk.Kind)) return apierrors.NewNotFound(schema.GroupResource{ Group: gvk.Group, Resource: resource, }, name) } func getGVK(gvkLabel string, gvk *schema.GroupVersionKind) error { parts := strings.Split(gvkLabel, ", Kind=") if len(parts) != 2 { return fmt.Errorf("invalid GVK format: %s", gvkLabel) } gvk.Group, gvk.Version = kv.Split(parts[0], "/") gvk.Kind = parts[1] return nil } func (o desiredSet) FindOwner(obj runtime.Object) (runtime.Object, error) { if obj == nil { return nil, ErrOwnerNotFound } meta, err := meta.Accessor(obj) if err != nil { return nil, err } var ( debugID = fmt.Sprintf("%s/%s", meta.GetNamespace(), meta.GetName()) gvkLabel = meta.GetAnnotations()[LabelGVK] namespace = meta.GetAnnotations()[LabelNamespace] name = meta.GetAnnotations()[LabelName] gvk schema.GroupVersionKind ) if gvkLabel == "" { return nil, ErrOwnerNotFound } if err := getGVK(gvkLabel, &gvk); err != nil { return nil, err } cache, client, err := o.getControllerAndClient(debugID, gvk) if err != nil { return nil, err } if cache != nil { return o.fromCache(cache, namespace, name, gvk) } return o.fromClient(client, namespace, name, gvk) } func (o *desiredSet) fromClient(client dynamic.NamespaceableResourceInterface, namespace, name string, gvk schema.GroupVersionKind) (runtime.Object, error) { var ( err error obj interface{} ) if namespace == "" { obj, err = client.Get(o.ctx, name, metav1.GetOptions{}) } else { obj, err = client.Namespace(namespace).Get(o.ctx, name, metav1.GetOptions{}) } if err != nil { return nil, err } if ro, ok := obj.(runtime.Object); ok { return ro, nil } return nil, notFound(name, gvk) } func (o *desiredSet) fromCache(cache cache.SharedInformer, namespace, name string, gvk schema.GroupVersionKind) (runtime.Object, error) { var key string if namespace == "" { key = name } else { key = namespace + "/" + name } item, ok, err := cache.GetStore().GetByKey(key) if err != nil { return nil, err } else if !ok { return nil, notFound(name, gvk) } else if ro, ok := item.(runtime.Object); ok { return ro, nil } return nil, notFound(name, gvk) } func (o desiredSet) PurgeOrphan(obj runtime.Object) error { if obj == nil { return nil } meta, err := meta.Accessor(obj) if err != nil { return err } if _, err := o.FindOwner(obj); apierrors.IsNotFound(err) { gvk, err := gvk.Get(obj) if err != nil { return err } o.strictCaching = false _, client, err := o.getControllerAndClient(meta.GetName(), gvk) if err != nil { return err } if meta.GetNamespace() == "" { return client.Delete(o.ctx, meta.GetName(), metav1.DeleteOptions{}) } return client.Namespace(meta.GetNamespace()).Delete(o.ctx, meta.GetName(), metav1.DeleteOptions{}) } else if err == ErrOwnerNotFound { return nil } else if err != nil { return err } return nil } ================================================ FILE: pkg/apply/desiredset_process.go ================================================ package apply import ( "context" "errors" "fmt" "sort" "sync" gvk2 "github.com/rancher/wrangler/v3/pkg/gvk" "github.com/rancher/wrangler/v3/pkg/merr" "github.com/rancher/wrangler/v3/pkg/objectset" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" errors2 "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" v1 "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" "k8s.io/apimachinery/pkg/runtime/schema" types2 "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/cache" ) var ( ErrReplace = errors.New("replace object with changes") ReplaceOnChange = func(name string, o runtime.Object, patchType types2.PatchType, data []byte, subresources ...string) (runtime.Object, error) { return nil, ErrReplace } ) func (o *desiredSet) getControllerAndClient(debugID string, gvk schema.GroupVersionKind) (cache.SharedIndexInformer, dynamic.NamespaceableResourceInterface, error) { // client needs to be accessed first so that the gvk->gvr mapping gets cached client, err := o.a.clients.client(gvk) if err != nil { return nil, nil, err } informer, ok := o.pruneTypes[gvk] if !ok { informer = o.a.informers[gvk] } if informer == nil && o.informerFactory != nil { newInformer, err := o.informerFactory.Get(gvk, o.a.clients.gvr(gvk)) if err != nil { return nil, nil, fmt.Errorf("failed to construct informer for %v for %s: %w", gvk, debugID, err) } informer = newInformer } if informer == nil && o.strictCaching { return nil, nil, fmt.Errorf("failed to find informer for %s for %s: %w", gvk, debugID, ErrNoInformerFound) } return informer, client, nil } func (o *desiredSet) assignOwnerReference(gvk schema.GroupVersionKind, objs objectset.ObjectByKey) error { if o.owner == nil { return fmt.Errorf("no owner set to assign owner reference") } ownerMeta, err := meta.Accessor(o.owner) if err != nil { return err } ownerGVK, err := gvk2.Get(o.owner) if err != nil { return err } ownerNSed, err := o.a.clients.IsNamespaced(ownerGVK) if err != nil { return err } for k, v := range objs { // can't set owners across boundaries if ownerNSed { if nsed, err := o.a.clients.IsNamespaced(gvk); err != nil { return err } else if !nsed { continue } } assignNS := false assignOwner := true if nsed, err := o.a.clients.IsNamespaced(gvk); err != nil { return err } else if nsed { if k.Namespace == "" { assignNS = true } else if k.Namespace != ownerMeta.GetNamespace() && ownerNSed { assignOwner = false } } if !assignOwner { continue } v = v.DeepCopyObject() meta, err := meta.Accessor(v) if err != nil { return err } if assignNS { meta.SetNamespace(ownerMeta.GetNamespace()) } shouldSet := true for _, of := range meta.GetOwnerReferences() { if ownerMeta.GetUID() == of.UID { shouldSet = false break } } if shouldSet { meta.SetOwnerReferences(append(meta.GetOwnerReferences(), v1.OwnerReference{ APIVersion: ownerGVK.GroupVersion().String(), Kind: ownerGVK.Kind, Name: ownerMeta.GetName(), UID: ownerMeta.GetUID(), Controller: &o.ownerReferenceController, BlockOwnerDeletion: &o.ownerReferenceBlock, })) } objs[k] = v if assignNS { delete(objs, k) k.Namespace = ownerMeta.GetNamespace() objs[k] = v } } return nil } func (o *desiredSet) adjustNamespace(gvk schema.GroupVersionKind, objs objectset.ObjectByKey) error { for k, v := range objs { if k.Namespace != "" { continue } v = v.DeepCopyObject() meta, err := meta.Accessor(v) if err != nil { return err } meta.SetNamespace(o.defaultNamespace) delete(objs, k) k.Namespace = o.defaultNamespace objs[k] = v } return nil } func (o *desiredSet) clearNamespace(objs objectset.ObjectByKey) error { for k, v := range objs { if k.Namespace == "" { continue } v = v.DeepCopyObject() meta, err := meta.Accessor(v) if err != nil { return err } meta.SetNamespace("") delete(objs, k) k.Namespace = "" objs[k] = v } return nil } func (o *desiredSet) createPatcher(client dynamic.NamespaceableResourceInterface) Patcher { return func(namespace, name string, pt types2.PatchType, data []byte) (object runtime.Object, e error) { if namespace != "" { return client.Namespace(namespace).Patch(o.ctx, name, pt, data, v1.PatchOptions{}) } return client.Patch(o.ctx, name, pt, data, v1.PatchOptions{}) } } func (o *desiredSet) filterCrossVersion(gvk schema.GroupVersionKind, keys []objectset.ObjectKey) []objectset.ObjectKey { result := make([]objectset.ObjectKey, 0, len(keys)) gk := gvk.GroupKind() for _, key := range keys { if o.objs.Contains(gk, key) { continue } if key.Namespace == o.defaultNamespace && o.objs.Contains(gk, objectset.ObjectKey{Name: key.Name}) { continue } result = append(result, key) } return result } func (o *desiredSet) process(debugID string, set labels.Selector, gvk schema.GroupVersionKind, objs objectset.ObjectByKey) { controller, client, err := o.getControllerAndClient(debugID, gvk) if err != nil { o.err(err) return } nsed, err := o.a.clients.IsNamespaced(gvk) if err != nil { o.err(err) return } if !nsed && o.restrictClusterScoped { o.err(fmt.Errorf("invalid cluster scoped gvk: %v", gvk)) return } if o.setOwnerReference && o.owner != nil { if err := o.assignOwnerReference(gvk, objs); err != nil { o.err(err) return } } if nsed { if err := o.adjustNamespace(gvk, objs); err != nil { o.err(err) return } } else { if err := o.clearNamespace(objs); err != nil { o.err(err) return } } patcher, ok := o.patchers[gvk] if !ok { patcher = o.createPatcher(client) } reconciler := o.reconcilers[gvk] existing, err := o.list(nsed, controller, client, set, objs) if err != nil { o.err(fmt.Errorf("failed to list %s for %s: %w", gvk, debugID, err)) return } toCreate, toDelete, toUpdate := compareSets(existing, objs) // check for resources in the objectset but under a different version of the same group/kind toDelete = o.filterCrossVersion(gvk, toDelete) if o.createPlan { o.plan.Create[gvk] = toCreate o.plan.Delete[gvk] = toDelete reconciler = nil patcher = func(namespace, name string, pt types2.PatchType, data []byte) (runtime.Object, error) { data, err := sanitizePatch(data, true) if err != nil { return nil, err } if string(data) != "{}" { o.plan.Update.Add(gvk, namespace, name, string(data)) } return nil, nil } toCreate = nil toDelete = nil } createF := func(k objectset.ObjectKey) { obj := objs[k] obj, err := prepareObjectForCreate(gvk, obj) if err != nil { o.err(fmt.Errorf("failed to prepare create %s %s for %s: %w", k, gvk, debugID, err)) return } _, err = o.create(nsed, k.Namespace, client, obj) if errors2.IsAlreadyExists(err) { // Taking over an object that wasn't previously managed by us existingObj, err := o.get(nsed, k.Namespace, k.Name, client) if err == nil { toUpdate = append(toUpdate, k) existing[k] = existingObj return } } if err != nil { o.err(fmt.Errorf("failed to create %s %s for %s: %w", k, gvk, debugID, err)) return } logrus.Debugf("DesiredSet - Created %s %s for %s", gvk, k, debugID) } deleteF := func(k objectset.ObjectKey, force bool) { if err := o.delete(nsed, k.Namespace, k.Name, client, force, gvk); err != nil { o.err(fmt.Errorf("failed to delete %s %s for %s: %w", k, gvk, debugID, err)) return } logrus.Debugf("DesiredSet - Delete %s %s for %s", gvk, k, debugID) } updateF := func(k objectset.ObjectKey) { err := o.compareObjects(gvk, reconciler, patcher, client, debugID, existing[k], objs[k], len(toCreate) > 0 || len(toDelete) > 0) if err == ErrReplace { deleteF(k, true) o.err(fmt.Errorf("DesiredSet - Replace Wait %s %s for %s", gvk, k, debugID)) } else if err != nil { o.err(fmt.Errorf("failed to update %s %s for %s: %w", k, gvk, debugID, err)) } } for _, k := range toCreate { createF(k) } for _, k := range toUpdate { updateF(k) } for _, k := range toDelete { deleteF(k, false) } } func (o *desiredSet) list(namespaced bool, informer cache.SharedIndexInformer, client dynamic.NamespaceableResourceInterface, selector labels.Selector, desiredObjects objectset.ObjectByKey) (map[objectset.ObjectKey]runtime.Object, error) { var ( errs []error objs = objectset.ObjectByKey{} ) if informer == nil { // If a lister namespace is set, assume all objects belong to the listerNamespace. If the // desiredSet has an owner but no lister namespace, list objects from all namespaces to ensure // we're cleaning up any owned resources. Otherwise, search only objects from the namespaces // used by the objects. Note: desiredSets without owners will never return objects to delete; // deletion requires an owner to track object references across multiple apply runs. var namespaces []string if o.listerNamespace != "" { namespaces = append(namespaces, o.listerNamespace) } else { namespaces = desiredObjects.Namespaces() } if o.owner != nil && o.listerNamespace == "" { // owner set and unspecified lister namespace, search all namespaces err := allNamespaceList(o.ctx, client, selector, func(obj unstructured.Unstructured) { if err := addObjectToMap(objs, &obj); err != nil { errs = append(errs, err) } }) if err != nil { errs = append(errs, err) } } else { // no owner or lister namespace intentionally restricted; only search in specified namespaces err := multiNamespaceList(o.ctx, namespaces, client, selector, func(obj unstructured.Unstructured) { if err := addObjectToMap(objs, &obj); err != nil { errs = append(errs, err) } }) if err != nil { errs = append(errs, err) } } return objs, merr.NewErrors(errs...) } var namespace string if namespaced { namespace = o.listerNamespace } // Special case for listing only by hash using indexers indexer := informer.GetIndexer() if hash, ok := getIndexableHash(indexer, selector); ok { return listByHash(indexer, hash, namespace) } if err := cache.ListAllByNamespace(indexer, namespace, selector, func(obj interface{}) { if err := addObjectToMap(objs, obj); err != nil { errs = append(errs, err) } }); err != nil { errs = append(errs, err) } return objs, merr.NewErrors(errs...) } func shouldPrune(obj runtime.Object) bool { meta, err := meta.Accessor(obj) if err != nil { return true } return meta.GetLabels()[LabelPrune] != "false" } func compareSets(existingSet, newSet objectset.ObjectByKey) (toCreate, toDelete, toUpdate []objectset.ObjectKey) { for k := range newSet { if _, ok := existingSet[k]; ok { toUpdate = append(toUpdate, k) } else { toCreate = append(toCreate, k) } } for k, obj := range existingSet { if _, ok := newSet[k]; !ok { if shouldPrune(obj) { toDelete = append(toDelete, k) } } } sortObjectKeys(toCreate) sortObjectKeys(toDelete) sortObjectKeys(toUpdate) return } func sortObjectKeys(keys []objectset.ObjectKey) { sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() }) } func addObjectToMap(objs objectset.ObjectByKey, obj interface{}) error { metadata, err := meta.Accessor(obj) if err != nil { return err } objs[objectset.ObjectKey{ Namespace: metadata.GetNamespace(), Name: metadata.GetName(), }] = obj.(runtime.Object) return nil } // allNamespaceList lists objects across all namespaces. func allNamespaceList(ctx context.Context, baseClient dynamic.NamespaceableResourceInterface, selector labels.Selector, appendFn func(obj unstructured.Unstructured)) error { list, err := baseClient.List(ctx, v1.ListOptions{ LabelSelector: selector.String(), }) if err != nil { return err } for _, obj := range list.Items { appendFn(obj) } return nil } // multiNamespaceList lists objects across all given namespaces, because requests are concurrent it is possible for appendFn to be called before errors are reported. func multiNamespaceList(ctx context.Context, namespaces []string, baseClient dynamic.NamespaceableResourceInterface, selector labels.Selector, appendFn func(obj unstructured.Unstructured)) error { var mu sync.Mutex wg, _ctx := errgroup.WithContext(ctx) // list all namespaces concurrently for _, namespace := range namespaces { namespace := namespace wg.Go(func() error { list, err := baseClient.Namespace(namespace).List(_ctx, v1.ListOptions{ LabelSelector: selector.String(), }) if err != nil { return err } mu.Lock() for _, obj := range list.Items { appendFn(obj) } mu.Unlock() return nil }) } return wg.Wait() } // getIndexableHash detects if provided selector can be replaced by using the hash index, if configured, in which case returns the hash value func getIndexableHash(indexer cache.Indexer, selector labels.Selector) (string, bool) { // Check if indexer was added if indexer == nil || indexer.GetIndexers()[byHash] == nil { return "", false } // Check specific case of listing with exact hash label selector if req, selectable := selector.Requirements(); len(req) != 1 || !selectable { return "", false } return selector.RequiresExactMatch(LabelHash) } // inNamespace checks whether a given object is a Kubernetes object and is part of the provided namespace func inNamespace(namespace string, obj interface{}) bool { metadata, err := meta.Accessor(obj) return err == nil && metadata.GetNamespace() == namespace } // listByHash use a pre-configured indexer to list objects of a certain type by their hash label func listByHash(indexer cache.Indexer, hash string, namespace string) (map[objectset.ObjectKey]runtime.Object, error) { var ( errs []error objs = objectset.ObjectByKey{} ) res, err := indexer.ByIndex(byHash, hash) if err != nil { return nil, err } for _, obj := range res { if namespace != "" && !inNamespace(namespace, obj) { continue } if err := addObjectToMap(objs, obj); err != nil { errs = append(errs, err) } } return objs, merr.NewErrors(errs...) } ================================================ FILE: pkg/apply/desiredset_process_test.go ================================================ package apply import ( "context" "errors" "strings" "testing" "github.com/rancher/wrangler/v3/pkg/objectset" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" 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" "k8s.io/client-go/dynamic/fake" k8stesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" ) func Test_multiNamespaceList(t *testing.T) { results := map[string]*unstructured.UnstructuredList{ "ns1": {Items: []unstructured.Unstructured{ {Object: map[string]interface{}{"name": "o1", "namespace": "ns1"}}, {Object: map[string]interface{}{"name": "o2", "namespace": "ns1"}}, {Object: map[string]interface{}{"name": "o3", "namespace": "ns1"}}, }}, "ns2": {Items: []unstructured.Unstructured{ {Object: map[string]interface{}{"name": "o4", "namespace": "ns2"}}, {Object: map[string]interface{}{"name": "o5", "namespace": "ns2"}}, }}, "ns3": {Items: []unstructured.Unstructured{}}, } s := runtime.NewScheme() err := appsv1.SchemeBuilder.AddToScheme(s) assert.NoError(t, err, "Failed to build schema.") baseClient := fake.NewSimpleDynamicClient(s) baseClient.PrependReactor("list", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { if strings.Contains(action.GetNamespace(), "error") { return true, nil, errors.New("simulated failure") } return true, results[action.GetNamespace()], nil }) type args struct { namespaces []string } tests := []struct { name string args args expectedCalls int expectError bool }{ { name: "no namespaces", args: args{ namespaces: []string{}, }, expectError: false, expectedCalls: 0, }, { name: "1 namespace", args: args{ namespaces: []string{"ns1"}, }, expectError: false, expectedCalls: 3, }, { name: "many namespaces", args: args{ namespaces: []string{"ns1", "ns2", "ns3"}, }, expectError: false, expectedCalls: 5, }, { name: "1 namespace error", args: args{ namespaces: []string{"error", "ns2", "ns3"}, }, expectError: true, expectedCalls: -1, }, { name: "many namespace errors", args: args{ namespaces: []string{"error", "error1", "error2"}, }, expectError: true, expectedCalls: -1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var calls int err := multiNamespaceList(context.TODO(), tt.args.namespaces, baseClient.Resource(appsv1.SchemeGroupVersion.WithResource("deployments")), labels.NewSelector(), func(obj unstructured.Unstructured) { calls += 1 }) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } if tt.expectedCalls >= 0 { assert.Equal(t, tt.expectedCalls, calls) } }) } } func Test_getIndexableHash(t *testing.T) { const hash = "somehash" hashSelector, err := GetSelector(map[string]string{LabelHash: hash}) if err != nil { t.Fatal(err) } envLabelSelector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"env": "dev"}}) if err != nil { t.Fatal(err) } indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{byHash: func(obj interface{}) ([]string, error) { return nil, nil }}) type args struct { indexer cache.Indexer selector labels.Selector } tests := []struct { name string args args wantHash string want bool }{ {name: "indexer configured", args: args{ indexer: indexer, selector: hashSelector, }, wantHash: hash, want: true}, {name: "indexer not configured", args: args{ indexer: cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}), selector: hashSelector, }, wantHash: "", want: false}, {name: "using Everything selector", args: args{ indexer: indexer, selector: labels.Everything(), }, wantHash: "", want: false}, {name: "using other label selectors", args: args{ indexer: indexer, selector: envLabelSelector, }, wantHash: "", want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotHash, got := getIndexableHash(tt.args.indexer, tt.args.selector) assert.Equalf(t, tt.wantHash, gotHash, "getIndexableHash(%v, %v)", tt.args.indexer, tt.args.selector) assert.Equalf(t, tt.want, got, "getIndexableHash(%v, %v)", tt.args.indexer, tt.args.selector) }) } } func Test_inNamespace(t *testing.T) { type args struct { namespace string obj interface{} } tests := []struct { name string args args want bool }{ {name: "object in namespace", args: args{ namespace: "ns", obj: &metav1.ObjectMeta{ Namespace: "ns", }, }, want: true}, {name: "object not in namespace", args: args{ namespace: "ns", obj: &metav1.ObjectMeta{ Namespace: "another-ns", }, }, want: false}, {name: "object not namespaced", args: args{ namespace: "ns", obj: &corev1.Namespace{}, }, want: false}, {name: "non k8s object", args: args{ namespace: "ns", obj: &struct{}{}, }, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.want, inNamespace(tt.args.namespace, tt.args.obj), "inNamespace(%v, %v)", tt.args.namespace, tt.args.obj) }) } } func Test_listByHash(t *testing.T) { indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) if err := addIndexerByHash(indexer); err != nil { t.Fatal(err) } addObject := func(name, namespace, hash string) *corev1.Pod { t.Helper() obj := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, Labels: map[string]string{LabelHash: hash}, }, } if err := indexer.Add(obj); err != nil { t.Fatal(err) } return obj } namespace := "ns" objects := []*corev1.Pod{ // 3 objects with the same hash addObject("obj0", namespace, "hash0"), addObject("obj01", namespace, "hash0"), addObject("obj02", "another-ns", "hash0"), // Single object for hash addObject("obj1", namespace, "hash1"), } type args struct { hash string namespace string } tests := []struct { name string args args want map[objectset.ObjectKey]runtime.Object }{ {name: "finds object by hash in all namespaces", args: args{ hash: "hash0", }, want: map[objectset.ObjectKey]runtime.Object{ objectset.NewObjectKey(objects[0]): objects[0], objectset.NewObjectKey(objects[1]): objects[1], objectset.NewObjectKey(objects[2]): objects[2], }}, {name: "finds object by hash in namespace", args: args{ hash: "hash0", namespace: namespace, }, want: map[objectset.ObjectKey]runtime.Object{ objectset.NewObjectKey(objects[0]): objects[0], objectset.NewObjectKey(objects[1]): objects[1], }}, {name: "returns empty if namespace does not match", args: args{ hash: "hash1", namespace: "another-ns", }, want: map[objectset.ObjectKey]runtime.Object{}}, {name: "finds object by hash", args: args{ hash: "hash1", namespace: namespace, }, want: map[objectset.ObjectKey]runtime.Object{ objectset.NewObjectKey(objects[3]): objects[3], }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := listByHash(indexer, tt.args.hash, tt.args.namespace) assert.NoError(t, err) assert.Equalf(t, tt.want, got, "listByHash(%v, %v, %v)", indexer, tt.args.hash, tt.args.namespace) }) } } ================================================ FILE: pkg/apply/fake/apply.go ================================================ package fake import ( "context" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/apply/injectors" "github.com/rancher/wrangler/v3/pkg/objectset" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) var _ apply.Apply = (*FakeApply)(nil) type FakeApply struct { Objects []*objectset.ObjectSet Count int } func (f *FakeApply) Apply(set *objectset.ObjectSet) error { f.Objects = append(f.Objects, set) f.Count++ return nil } func (f *FakeApply) ApplyObjects(objs ...runtime.Object) error { os := objectset.NewObjectSet() os.Add(objs...) return f.Apply(os) } func (f *FakeApply) WithCacheTypes(igs ...apply.InformerGetter) apply.Apply { return f } func (f *FakeApply) WithIgnorePreviousApplied() apply.Apply { return f } func (f *FakeApply) WithGVK(gvks ...schema.GroupVersionKind) apply.Apply { return f } func (f *FakeApply) WithSetID(id string) apply.Apply { return f } func (f *FakeApply) WithOwner(obj runtime.Object) apply.Apply { return f } func (f *FakeApply) WithInjector(injs ...injectors.ConfigInjector) apply.Apply { return f } func (f *FakeApply) WithInjectorName(injs ...string) apply.Apply { return f } func (f *FakeApply) WithPatcher(gvk schema.GroupVersionKind, patchers apply.Patcher) apply.Apply { return f } func (f *FakeApply) WithReconciler(gvk schema.GroupVersionKind, reconciler apply.Reconciler) apply.Apply { return f } func (f *FakeApply) WithStrictCaching() apply.Apply { return f } func (f *FakeApply) WithDynamicLookup() apply.Apply { return f } func (f *FakeApply) WithDefaultNamespace(ns string) apply.Apply { return f } func (f *FakeApply) WithListerNamespace(ns string) apply.Apply { return f } func (f *FakeApply) WithRestrictClusterScoped() apply.Apply { return f } func (f *FakeApply) WithSetOwnerReference(controller, block bool) apply.Apply { return f } func (f *FakeApply) WithRateLimiting(ratelimitingQPS float32) apply.Apply { return f } func (f *FakeApply) WithNoDelete() apply.Apply { return f } func (f *FakeApply) WithNoDeleteGVK(gvks ...schema.GroupVersionKind) apply.Apply { return f } func (f *FakeApply) WithContext(ctx context.Context) apply.Apply { return f } func (f *FakeApply) WithCacheTypeFactory(factory apply.InformerFactory) apply.Apply { return f } func (f *FakeApply) DryRun(objs ...runtime.Object) (apply.Plan, error) { return apply.Plan{}, nil } func (f *FakeApply) FindOwner(obj runtime.Object) (runtime.Object, error) { return nil, nil } func (f *FakeApply) PurgeOrphan(obj runtime.Object) error { return nil } func (f *FakeApply) WithOwnerKey(key string, gvk schema.GroupVersionKind) apply.Apply { return f } func (f *FakeApply) WithDiffPatch(gvk schema.GroupVersionKind, namespace, name string, patch []byte) apply.Apply { return f } ================================================ FILE: pkg/apply/injectors/registry.go ================================================ package injectors import "k8s.io/apimachinery/pkg/runtime" var ( injectors = map[string]ConfigInjector{} order []string ) type ConfigInjector func(config []runtime.Object) ([]runtime.Object, error) func Register(name string, injector ConfigInjector) { if _, ok := injectors[name]; !ok { order = append(order, name) } injectors[name] = injector } func Get(name string) ConfigInjector { return injectors[name] } ================================================ FILE: pkg/apply/reconcilers.go ================================================ package apply import ( "encoding/json" "fmt" "reflect" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) var ( defaultReconcilers = map[schema.GroupVersionKind]Reconciler{ v1.SchemeGroupVersion.WithKind("Secret"): reconcileSecret, v1.SchemeGroupVersion.WithKind("Service"): reconcileService, batchv1.SchemeGroupVersion.WithKind("Job"): reconcileJob, appsv1.SchemeGroupVersion.WithKind("Deployment"): reconcileDeployment, appsv1.SchemeGroupVersion.WithKind("DaemonSet"): reconcileDaemonSet, } ) func reconcileDaemonSet(oldObj, newObj runtime.Object) (bool, error) { oldSvc, ok := oldObj.(*appsv1.DaemonSet) if !ok { oldSvc = &appsv1.DaemonSet{} if err := convertObj(oldObj, oldSvc); err != nil { return false, err } } newSvc, ok := newObj.(*appsv1.DaemonSet) if !ok { newSvc = &appsv1.DaemonSet{} if err := convertObj(newObj, newSvc); err != nil { return false, err } } if !equality.Semantic.DeepEqual(oldSvc.Spec.Selector, newSvc.Spec.Selector) { return false, ErrReplace } return false, nil } func reconcileDeployment(oldObj, newObj runtime.Object) (bool, error) { oldSvc, ok := oldObj.(*appsv1.Deployment) if !ok { oldSvc = &appsv1.Deployment{} if err := convertObj(oldObj, oldSvc); err != nil { return false, err } } newSvc, ok := newObj.(*appsv1.Deployment) if !ok { newSvc = &appsv1.Deployment{} if err := convertObj(newObj, newSvc); err != nil { return false, err } } if !equality.Semantic.DeepEqual(oldSvc.Spec.Selector, newSvc.Spec.Selector) { return false, ErrReplace } return false, nil } func reconcileSecret(oldObj, newObj runtime.Object) (bool, error) { oldSvc, ok := oldObj.(*v1.Secret) if !ok { oldSvc = &v1.Secret{} if err := convertObj(oldObj, oldSvc); err != nil { return false, err } } newSvc, ok := newObj.(*v1.Secret) if !ok { newSvc = &v1.Secret{} if err := convertObj(newObj, newSvc); err != nil { return false, err } } if newSvc.Type != "" && oldSvc.Type != newSvc.Type { return false, ErrReplace } return false, nil } func reconcileService(oldObj, newObj runtime.Object) (bool, error) { oldSvc, ok := oldObj.(*v1.Service) if !ok { oldSvc = &v1.Service{} if err := convertObj(oldObj, oldSvc); err != nil { return false, err } } newSvc, ok := newObj.(*v1.Service) if !ok { newSvc = &v1.Service{} if err := convertObj(newObj, newSvc); err != nil { return false, err } } if newSvc.Spec.Type != "" && oldSvc.Spec.Type != newSvc.Spec.Type { return false, ErrReplace } return false, nil } func reconcileJob(oldObj, newObj runtime.Object) (bool, error) { oldJob, ok := oldObj.(*batchv1.Job) if !ok { oldJob = &batchv1.Job{} if err := convertObj(oldObj, oldJob); err != nil { return false, err } } newJob, ok := newObj.(*batchv1.Job) if !ok { newJob = &batchv1.Job{} if err := convertObj(newObj, newJob); err != nil { return false, err } } // We round trip the object here because when serializing to the applied // annotation values are truncated to 64 bytes. prunedJob, err := getOriginalObject(newJob.GroupVersionKind(), newJob) if err != nil { return false, err } newPrunedJob := &batchv1.Job{} if err := convertObj(prunedJob, newPrunedJob); err != nil { return false, err } if !equality.Semantic.DeepEqual(oldJob.Spec.Template, newPrunedJob.Spec.Template) { return false, ErrReplace } return false, nil } func convertObj(src interface{}, obj interface{}) error { uObj, ok := src.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected unstructured but got %v", reflect.TypeOf(src)) } bytes, err := uObj.MarshalJSON() if err != nil { return err } return json.Unmarshal(bytes, obj) } ================================================ FILE: pkg/broadcast/generic.go ================================================ package broadcast import ( "context" "sync" ) type ConnectFunc func() (chan interface{}, error) type Broadcaster struct { sync.Mutex running bool subs map[chan interface{}]struct{} } func (b *Broadcaster) Subscribe(ctx context.Context, connect ConnectFunc) (chan interface{}, error) { b.Lock() defer b.Unlock() if !b.running { if err := b.start(connect); err != nil { return nil, err } } sub := make(chan interface{}, 100) if b.subs == nil { b.subs = map[chan interface{}]struct{}{} } b.subs[sub] = struct{}{} go func() { <-ctx.Done() b.unsub(sub, true) }() return sub, nil } func (b *Broadcaster) unsub(sub chan interface{}, lock bool) { if lock { b.Lock() } if _, ok := b.subs[sub]; ok { close(sub) delete(b.subs, sub) } if lock { b.Unlock() } } func (b *Broadcaster) start(connect ConnectFunc) error { c, err := connect() if err != nil { return err } go b.stream(c) b.running = true return nil } func (b *Broadcaster) stream(input chan interface{}) { for item := range input { b.Lock() for sub := range b.subs { select { case sub <- item: default: // Slow consumer, drop go b.unsub(sub, true) } } b.Unlock() } b.Lock() for sub := range b.subs { b.unsub(sub, false) } b.running = false b.Unlock() } ================================================ FILE: pkg/cleanup/cleanup.go ================================================ package cleanup import ( "fmt" "os" "path/filepath" "strings" ) func Cleanup(path string) error { return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { fmt.Println(path) if err != nil { return err } if strings.Contains(path, "vendor") { return filepath.SkipDir } if strings.HasPrefix(info.Name(), "zz_generated") { fmt.Println("Removing", path) if err := os.Remove(path); err != nil { return err } } return nil }) } ================================================ FILE: pkg/clients/clients.go ================================================ package clients import ( "context" "time" "github.com/rancher/lasso/pkg/controller" "github.com/rancher/lasso/pkg/dynamic" "github.com/rancher/wrangler/v3/pkg/apply" admissionreg "github.com/rancher/wrangler/v3/pkg/generated/controllers/admissionregistration.k8s.io" admissionregcontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/admissionregistration.k8s.io/v1" "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io" crdcontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1" "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io" apicontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io/v1" "github.com/rancher/wrangler/v3/pkg/generated/controllers/apps" appcontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/apps/v1" "github.com/rancher/wrangler/v3/pkg/generated/controllers/batch" batchcontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/batch/v1" "github.com/rancher/wrangler/v3/pkg/generated/controllers/core" corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac" rbaccontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/ratelimit" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" ) type Clients struct { K8s kubernetes.Interface Core corecontrollers.Interface RBAC rbaccontrollers.Interface Apps appcontrollers.Interface CRD crdcontrollers.Interface API apicontrollers.Interface Admission admissionregcontrollers.Interface Batch batchcontrollers.Interface Apply apply.Apply Dynamic *dynamic.Controller ClientConfig clientcmd.ClientConfig RESTConfig *rest.Config CachedDiscovery discovery.CachedDiscoveryInterface SharedControllerFactory controller.SharedControllerFactory RESTMapper meta.RESTMapper FactoryOptions *generic.FactoryOptions } func ensureSharedFactory(cfg *rest.Config, opts *generic.FactoryOptions) (*generic.FactoryOptions, error) { if opts == nil { opts = &generic.FactoryOptions{} } copy := *opts factory, err := generic.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } copy.SharedControllerFactory = factory.ControllerFactory() copy.SharedCacheFactory = factory.ControllerFactory().SharedCacheFactory() return ©, nil } func New(clientConfig clientcmd.ClientConfig, opts *generic.FactoryOptions) (*Clients, error) { cfg, err := clientConfig.ClientConfig() if err != nil { return nil, err } clients, err := NewFromConfig(cfg, opts) if err != nil { return nil, err } clients.ClientConfig = clientConfig return clients, nil } func NewFromConfig(cfg *rest.Config, opts *generic.FactoryOptions) (*Clients, error) { cfg = restConfigDefaults(cfg) opts, err := ensureSharedFactory(cfg, opts) if err != nil { return nil, err } core, err := core.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } rbac, err := rbac.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } apps, err := apps.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } api, err := apiregistration.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } crd, err := apiextensions.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } adminReg, err := admissionreg.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } k8s, err := kubernetes.NewForConfig(cfg) if err != nil { return nil, err } batch, err := batch.NewFactoryFromConfigWithOptions(cfg, opts) if err != nil { return nil, err } cache := memory.NewMemCacheClient(k8s.Discovery()) restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cache) apply, err := apply.NewForConfig(cfg) if err != nil { return nil, err } return &Clients{ K8s: k8s, Core: core.Core().V1(), RBAC: rbac.Rbac().V1(), Apps: apps.Apps().V1(), CRD: crd.Apiextensions().V1(), API: api.Apiregistration().V1(), Admission: adminReg.Admissionregistration().V1(), Batch: batch.Batch().V1(), Apply: apply.WithSetOwnerReference(false, false), RESTConfig: cfg, CachedDiscovery: cache, SharedControllerFactory: opts.SharedControllerFactory, RESTMapper: restMapper, FactoryOptions: opts, Dynamic: dynamic.New(k8s.Discovery()), }, nil } func (c *Clients) ToRawKubeConfigLoader() clientcmd.ClientConfig { return c.ClientConfig } func (c *Clients) ToRESTConfig() (*rest.Config, error) { return c.RESTConfig, nil } func (c *Clients) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { return c.CachedDiscovery, nil } func (c *Clients) ToRESTMapper() (meta.RESTMapper, error) { return c.RESTMapper, nil } func (c *Clients) Start(ctx context.Context) error { if err := c.Dynamic.Register(ctx, c.SharedControllerFactory); err != nil { return err } return c.SharedControllerFactory.Start(ctx, 5) } func restConfigDefaults(cfg *rest.Config) *rest.Config { cfg = rest.CopyConfig(cfg) cfg.Timeout = 15 * time.Minute cfg.RateLimiter = ratelimit.None return cfg } ================================================ FILE: pkg/codegen/main.go ================================================ package main import ( controllergen "github.com/rancher/wrangler/v3/pkg/controller-gen" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" coordinationv1 "k8s.io/api/coordination/v1" v1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" storagev1 "k8s.io/api/storage/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" ) func main() { controllergen.Run(args.Options{ ImportPackage: "github.com/rancher/wrangler/v3/pkg/generated", OutputPackage: "github.com/rancher/wrangler/pkg/generated", Boilerplate: "scripts/boilerplate.go.txt", Groups: map[string]args.Group{ v1.GroupName: { Types: []interface{}{ v1.Event{}, v1.Node{}, v1.Namespace{}, v1.Secret{}, v1.Service{}, v1.ServiceAccount{}, v1.Endpoints{}, v1.ConfigMap{}, v1.PersistentVolume{}, v1.PersistentVolumeClaim{}, v1.Pod{}, v1.LimitRange{}, v1.ResourceQuota{}, }, }, discoveryv1.GroupName: { Types: []interface{}{ discoveryv1.EndpointSlice{}, }, OutputControllerPackageName: "discovery", }, extensionsv1beta1.GroupName: { Types: []interface{}{ extensionsv1beta1.Ingress{}, }, }, rbacv1.GroupName: { Types: []interface{}{ rbacv1.Role{}, rbacv1.RoleBinding{}, rbacv1.ClusterRole{}, rbacv1.ClusterRoleBinding{}, }, OutputControllerPackageName: "rbac", }, appsv1.GroupName: { Types: []interface{}{ appsv1.Deployment{}, appsv1.DaemonSet{}, appsv1.StatefulSet{}, }, }, storagev1.GroupName: { OutputControllerPackageName: "storage", Types: []interface{}{ storagev1.StorageClass{}, }, }, apiextv1.GroupName: { Types: []interface{}{ apiextv1.CustomResourceDefinition{}, }, }, apiv1.GroupName: { Types: []interface{}{ apiv1.APIService{}, }, }, batchv1.GroupName: { Types: []interface{}{ batchv1.Job{}, }, }, networkingv1.GroupName: { Types: []interface{}{ networkingv1.NetworkPolicy{}, }, }, admissionregistrationv1.GroupName: { Types: []interface{}{ admissionregistrationv1.ValidatingWebhookConfiguration{}, admissionregistrationv1.MutatingWebhookConfiguration{}, }, }, coordinationv1.GroupName: { Types: []interface{}{ coordinationv1.Lease{}, }, }, }, }) } ================================================ FILE: pkg/condition/condition.go ================================================ package condition import ( "reflect" "time" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/sirupsen/logrus" ) type Cond string func (c Cond) GetStatus(obj interface{}) string { return getStatus(obj, string(c)) } func (c Cond) SetError(obj interface{}, reason string, err error) { if err == nil || err == generic.ErrSkip { c.True(obj) c.Message(obj, "") c.Reason(obj, reason) return } if reason == "" { reason = "Error" } c.False(obj) c.Message(obj, err.Error()) c.Reason(obj, reason) } func (c Cond) MatchesError(obj interface{}, reason string, err error) bool { if err == nil { return c.IsTrue(obj) && c.GetMessage(obj) == "" && c.GetReason(obj) == reason } if reason == "" { reason = "Error" } return c.IsFalse(obj) && c.GetMessage(obj) == err.Error() && c.GetReason(obj) == reason } func (c Cond) SetStatus(obj interface{}, status string) { setStatus(obj, string(c), status) } func (c Cond) SetStatusBool(obj interface{}, val bool) { if val { setStatus(obj, string(c), "True") } else { setStatus(obj, string(c), "False") } } func (c Cond) True(obj interface{}) { setStatus(obj, string(c), "True") } func (c Cond) IsTrue(obj interface{}) bool { return getStatus(obj, string(c)) == "True" } func (c Cond) False(obj interface{}) { setStatus(obj, string(c), "False") } func (c Cond) IsFalse(obj interface{}) bool { return getStatus(obj, string(c)) == "False" } func (c Cond) Unknown(obj interface{}) { setStatus(obj, string(c), "Unknown") } func (c Cond) IsUnknown(obj interface{}) bool { return getStatus(obj, string(c)) == "Unknown" } func (c Cond) LastUpdated(obj interface{}, ts string) { setTS(obj, string(c), ts) } func (c Cond) GetLastUpdated(obj interface{}) string { return getTS(obj, string(c)) } func (c Cond) CreateUnknownIfNotExists(obj interface{}) { condSlice := getValue(obj, "Status", "Conditions") if !condSlice.IsValid() { condSlice = getValue(obj, "Conditions") } cond := findCond(obj, condSlice, string(c)) if cond == nil { c.Unknown(obj) } } func (c Cond) Reason(obj interface{}, reason string) { cond := findOrCreateCond(obj, string(c)) getFieldValue(cond, "Reason").SetString(reason) } func (c Cond) GetReason(obj interface{}) string { cond := findOrNotCreateCond(obj, string(c)) if cond == nil { return "" } return getFieldValue(*cond, "Reason").String() } func (c Cond) SetMessageIfBlank(obj interface{}, message string) { if c.GetMessage(obj) == "" { c.Message(obj, message) } } func (c Cond) Message(obj interface{}, message string) { cond := findOrCreateCond(obj, string(c)) setValue(cond, "Message", message) } func (c Cond) GetMessage(obj interface{}) string { cond := findOrNotCreateCond(obj, string(c)) if cond == nil { return "" } return getFieldValue(*cond, "Message").String() } func touchTS(value reflect.Value) { now := time.Now().UTC().Format(time.RFC3339) getFieldValue(value, "LastUpdateTime").SetString(now) } func getStatus(obj interface{}, condName string) string { cond := findOrNotCreateCond(obj, condName) if cond == nil { return "" } return getFieldValue(*cond, "Status").String() } func setTS(obj interface{}, condName, ts string) { cond := findOrCreateCond(obj, condName) getFieldValue(cond, "LastUpdateTime").SetString(ts) } func getTS(obj interface{}, condName string) string { cond := findOrNotCreateCond(obj, condName) if cond == nil { return "" } return getFieldValue(*cond, "LastUpdateTime").String() } func setStatus(obj interface{}, condName, status string) { if reflect.TypeOf(obj).Kind() != reflect.Ptr { panic("obj passed must be a pointer") } cond := findOrCreateCond(obj, condName) setValue(cond, "Status", status) } func setValue(cond reflect.Value, fieldName, newValue string) { value := getFieldValue(cond, fieldName) if value.String() != newValue { value.SetString(newValue) touchTS(cond) } } func findOrNotCreateCond(obj interface{}, condName string) *reflect.Value { condSlice := getValue(obj, "Status", "Conditions") if !condSlice.IsValid() { condSlice = getValue(obj, "Conditions") } return findCond(obj, condSlice, condName) } func findOrCreateCond(obj interface{}, condName string) reflect.Value { condSlice := getValue(obj, "Status", "Conditions") if !condSlice.IsValid() { condSlice = getValue(obj, "Conditions") } cond := findCond(obj, condSlice, condName) if cond != nil { return *cond } newCond := reflect.New(condSlice.Type().Elem()).Elem() newCond.FieldByName("Type").SetString(condName) newCond.FieldByName("Status").SetString("Unknown") condSlice.Set(reflect.Append(condSlice, newCond)) return *findCond(obj, condSlice, condName) } func findCond(obj interface{}, val reflect.Value, name string) *reflect.Value { defer func() { if recover() != nil { logrus.Fatalf("failed to find .Status.Conditions field on %v", reflect.TypeOf(obj)) } }() for i := 0; i < val.Len(); i++ { cond := val.Index(i) typeVal := getFieldValue(cond, "Type") if typeVal.String() == name { return &cond } } return nil } func getValue(obj interface{}, name ...string) reflect.Value { if obj == nil { return reflect.Value{} } v := reflect.ValueOf(obj) t := v.Type() if t.Kind() == reflect.Ptr { v = v.Elem() } field := v.FieldByName(name[0]) if len(name) == 1 { return field } return getFieldValue(field, name[1:]...) } func getFieldValue(v reflect.Value, name ...string) reflect.Value { if !v.IsValid() { return v } field := v.FieldByName(name[0]) if len(name) == 1 { return field } return getFieldValue(field, name[1:]...) } func Error(reason string, err error) error { return &conditionError{ reason: reason, message: err.Error(), } } type conditionError struct { reason string message string } func (e *conditionError) Error() string { return e.message } ================================================ FILE: pkg/controller-gen/OWNERS ================================================ # See the OWNERS docs at https://go.k8s.io/owners approvers: - lavalamp - wojtek-t - caesarxuchao reviewers: - lavalamp - wojtek-t - caesarxuchao ================================================ FILE: pkg/controller-gen/README.md ================================================ See [generating-clientset.md](https://git.k8s.io/community/contributors/devel/sig-api-machinery/generating-clientset.md) [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/staging/src/k8s.io/code-generator/client-gen/README.md?pixel)]() ================================================ FILE: pkg/controller-gen/args/args.go ================================================ package args import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/gengo/v2/types" ) type CustomArgs struct { // Package is the directory path where generated code output will be located Package string ImportPackage string TypesByGroup map[schema.GroupVersion][]*types.Name Options Options OutputBase string // BoilerplateContent is the actual boilerplate content that has been // read. It will be added to every generated files. BoilerplateContent []byte } type Options struct { ImportPackage string // OutputPackage is the directory path where generated code output will be located OutputPackage string Groups map[string]Group // Boilerplate is the filepath to a boilerplate file whose content will // be added to every generated files. Boilerplate string } type Type struct { Version string Package string Name string } type Group struct { // Types is a slice of the following types // Instance of any struct: used for reflection to describe the type // string: a directory that will be listed (non-recursively) for types // Type: a description of a type Types []interface{} GenerateTypes bool // Generate clientsets GenerateClients bool OutputControllerPackageName string // Generate listers GenerateListers bool // Generate informers GenerateInformers bool // Generate openapi GenerateOpenAPI bool // Open API model package name OpenAPIModelPackageName string // OpenAPI extra dependencies OpenAPIDependencies []string // The package name of the API types PackageName string } ================================================ FILE: pkg/controller-gen/args/groupversion.go ================================================ package args import ( gotypes "go/types" "reflect" "sort" "strings" "golang.org/x/tools/go/packages" "golang.org/x/tools/imports" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/code-generator/cmd/client-gen/generators/util" "k8s.io/gengo/v2/types" ) const ( needsComment = ` // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object ` objectComment = "+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object" ) func translate(types []Type, err error) (result []interface{}, _ error) { for _, v := range types { result = append(result, v) } return result, err } func ObjectsToGroupVersion(group string, objs []interface{}, ret map[schema.GroupVersion][]*types.Name) error { for _, obj := range objs { if s, ok := obj.(string); ok { types, err := translate(ScanDirectory(s)) if err != nil { return err } if err := ObjectsToGroupVersion(group, types, ret); err != nil { return err } continue } version, t := toVersionType(obj) gv := schema.GroupVersion{ Group: group, Version: version, } ret[gv] = append(ret[gv], t) } return nil } func toVersionType(obj interface{}) (string, *types.Name) { switch v := obj.(type) { case Type: return v.Version, &types.Name{ Package: v.Package, Name: v.Name, } case *Type: return v.Version, &types.Name{ Package: v.Package, Name: v.Name, } } t := reflect.TypeOf(obj) if t.Kind() == reflect.Ptr { t = t.Elem() } pkg := imports.VendorlessPath(t.PkgPath()) return versionFromPackage(pkg), &types.Name{ Package: pkg, Name: t.Name(), } } func versionFromPackage(pkg string) string { parts := strings.Split(pkg, "/") return parts[len(parts)-1] } func CheckType(passedType *types.Type) { tags := util.MustParseClientGenTags(passedType.SecondClosestCommentLines) if !tags.GenerateClient { panic("Type " + passedType.String() + " is missing comment " + needsComment) } found := false for _, line := range passedType.SecondClosestCommentLines { if strings.Contains(line, objectComment) { found = true } } if !found { panic("Type " + passedType.String() + " is missing comment " + objectComment) } } func ScanDirectory(pkgPath string) (result []Type, err error) { pkgs, err := packages.Load(&packages.Config{ Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax, Dir: pkgPath, }) if err != nil { return nil, err } for _, v := range pkgs[0].TypesInfo.Defs { typeAndName, ok := v.(*gotypes.TypeName) if !ok { continue } s, ok := typeAndName.Type().Underlying().(*gotypes.Struct) if !ok { continue } for i := 0; i < s.NumFields(); i++ { f := s.Field(i) if f.Embedded() && f.Type().String() == "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" { pkgPath := imports.VendorlessPath(pkgs[0].PkgPath) result = append(result, Type{ Package: pkgPath, Version: versionFromPackage(pkgPath), Name: typeAndName.Name(), }) } } } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return } ================================================ FILE: pkg/controller-gen/args/groupversion_test.go ================================================ package args import ( "fmt" "os" "testing" ) func TestScan(t *testing.T) { cwd, _ := os.Getwd() fmt.Println(cwd) _, _ = ScanDirectory("./testdata") } ================================================ FILE: pkg/controller-gen/args/testdata/test.go ================================================ package testdata import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type SomeStruct struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` } ================================================ FILE: pkg/controller-gen/generators/client_generator.go ================================================ /* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package generators import ( "fmt" "path/filepath" "strings" args "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/types" ) type ClientGenerator struct { Fakes map[string][]string } func NewClientGenerator() *ClientGenerator { return &ClientGenerator{ Fakes: make(map[string][]string), } } // Packages makes the client package definition. func (cg *ClientGenerator) GetTargets(context *generator.Context, customArgs *args.CustomArgs) []generator.Target { generateTypesGroups := map[string]bool{} for groupName, group := range customArgs.Options.Groups { if group.GenerateTypes { generateTypesGroups[groupName] = true } } var ( packageList []generator.Target groups = map[string]bool{} ) for gv, types := range customArgs.TypesByGroup { if !groups[gv.Group] { packageList = append(packageList, cg.groupPackage(gv.Group, customArgs)) if generateTypesGroups[gv.Group] { packageList = append(packageList, cg.typesGroupPackage(types[0], gv, customArgs)) } } groups[gv.Group] = true packageList = append(packageList, cg.groupVersionPackage(gv, customArgs)) if generateTypesGroups[gv.Group] { packageList = append(packageList, cg.typesGroupVersionPackage(types[0], gv, customArgs)) packageList = append(packageList, cg.typesGroupVersionDocPackage(types[0], gv, customArgs)) } } return packageList } func (cg *ClientGenerator) typesGroupPackage(name *types.Name, gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Target { packagePath := strings.TrimSuffix(name.Package, "/"+gv.Version) return Target(customArgs, packagePath, func(context *generator.Context) []generator.Generator { return []generator.Generator{ RegisterGroupGo(gv.Group, customArgs), } }) } func (cg *ClientGenerator) typesGroupVersionDocPackage(name *types.Name, gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Target { packagePath := name.Package p := Target(customArgs, packagePath, func(context *generator.Context) []generator.Generator { return []generator.Generator{ generator.GoGenerator{ OutputFilename: "doc.go", }, RegisterGroupVersionGo(gv, customArgs), ListTypesGo(gv, customArgs), } }) openAPIDirective := "" if customArgs.Options.Groups[gv.Group].GenerateOpenAPI { openAPIDirective = fmt.Sprintf("\n// +k8s:openapi-gen=true") } if customArgs.Options.Groups[gv.Group].OpenAPIModelPackageName != "" { openAPIDirective += fmt.Sprintf("\n// +k8s:openapi-model-package=%s", customArgs.Options.Groups[gv.Group].OpenAPIModelPackageName) } p.HeaderComment = []byte(fmt.Sprintf(` %s %s // +k8s:deepcopy-gen=package // +groupName=%s `, string(customArgs.BoilerplateContent), openAPIDirective, gv.Group)) return p } func (cg *ClientGenerator) typesGroupVersionPackage(name *types.Name, gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Target { packagePath := name.Package return Target(customArgs, packagePath, func(context *generator.Context) []generator.Generator { return []generator.Generator{ RegisterGroupVersionGo(gv, customArgs), ListTypesGo(gv, customArgs), } }) } func (cg *ClientGenerator) groupPackage(group string, customArgs *args.CustomArgs) generator.Target { packagePath := filepath.Join(customArgs.Package, "controllers", groupPackageName(group, customArgs.Options.Groups[group].OutputControllerPackageName)) return Target(customArgs, packagePath, func(context *generator.Context) []generator.Generator { return []generator.Generator{ FactoryGo(group, customArgs), GroupInterfaceGo(group, customArgs), } }) } func (cg *ClientGenerator) groupVersionPackage(gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Target { packagePath := filepath.Join(customArgs.Package, "controllers", groupPackageName(gv.Group, customArgs.Options.Groups[gv.Group].OutputControllerPackageName), gv.Version) return Target(customArgs, packagePath, func(context *generator.Context) []generator.Generator { generators := []generator.Generator{ GroupVersionInterfaceGo(gv, customArgs), } for _, t := range customArgs.TypesByGroup[gv] { generators = append(generators, TypeGo(gv, t, customArgs)) cg.Fakes[packagePath] = append(cg.Fakes[packagePath], t.Name) } return generators }) } ================================================ FILE: pkg/controller-gen/generators/factory_go.go ================================================ package generators import ( "fmt" "io" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/gengo/v2/generator" ) func FactoryGo(group string, customArgs *args.CustomArgs) generator.Generator { return &factory{ group: group, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: "factory.go", OptionalBody: []byte(factoryBody), }, } } type factory struct { generator.GoGenerator group string customArgs *args.CustomArgs } func (f *factory) Imports(*generator.Context) []string { imports := Imports for gv, types := range f.customArgs.TypesByGroup { if f.group == gv.Group && len(types) > 0 { imports = append(imports, fmt.Sprintf("%s \"%s\"", gv.Version, types[0].Package)) } } return imports } func (f *factory) Init(c *generator.Context, w io.Writer) error { if err := f.GoGenerator.Init(c, w); err != nil { return err } sw := generator.NewSnippetWriter(w, c, "{{", "}}") m := map[string]interface{}{ "groupName": upperLowercase(f.group), } sw.Do("\n\nfunc (c *Factory) {{.groupName}}() Interface {\n", m) sw.Do(" return New(c.ControllerFactory())\n", m) sw.Do("}\n\n", m) sw.Do("\n\nfunc (c *Factory) WithAgent(userAgent string) Interface {\n", m) sw.Do(" return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory()))\n", m) sw.Do("}\n\n", m) return sw.Error() } var factoryBody = ` type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } ` ================================================ FILE: pkg/controller-gen/generators/group_interface_go.go ================================================ package generators import ( "fmt" "io" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" ) func GroupInterfaceGo(group string, customArgs *args.CustomArgs) generator.Generator { return &interfaceGo{ group: group, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: "interface.go", OptionalBody: []byte(interfaceBody), }, } } type interfaceGo struct { generator.GoGenerator group string customArgs *args.CustomArgs } func (f *interfaceGo) Imports(context *generator.Context) []string { packages := Imports for gv := range f.customArgs.TypesByGroup { if gv.Group != f.group { continue } pkg := f.customArgs.ImportPackage if pkg == "" { pkg = f.customArgs.Package } packages = append(packages, fmt.Sprintf("%s \"%s/controllers/%s/%s\"", gv.Version, pkg, groupPackageName(gv.Group, f.customArgs.Options.Groups[gv.Group].OutputControllerPackageName), gv.Version)) } return packages } func (f *interfaceGo) Init(c *generator.Context, w io.Writer) error { sw := generator.NewSnippetWriter(w, c, "{{", "}}") sw.Do("type Interface interface {\n", nil) for gv := range f.customArgs.TypesByGroup { if gv.Group != f.group { continue } sw.Do("{{.upperVersion}}() {{.version}}.Interface\n", map[string]interface{}{ "upperVersion": namer.IC(gv.Version), "version": gv.Version, }) } sw.Do("}\n", nil) if err := f.GoGenerator.Init(c, w); err != nil { return err } for gv := range f.customArgs.TypesByGroup { if gv.Group != f.group { continue } m := map[string]interface{}{ "upperGroup": upperLowercase(f.group), "upperVersion": namer.IC(gv.Version), "version": gv.Version, } sw.Do("\nfunc (g *group) {{.upperVersion}}() {{.version}}.Interface {\n", m) sw.Do("return {{.version}}.New(g.controllerFactory)\n", m) sw.Do("}\n", m) } return sw.Error() } var interfaceBody = ` type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } ` ================================================ FILE: pkg/controller-gen/generators/group_version_interface_go.go ================================================ package generators import ( "fmt" "io" "strings" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" "k8s.io/gengo/v2/types" ) func GroupVersionInterfaceGo(gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Generator { return &groupInterfaceGo{ gv: gv, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: "interface.go", }, } } type groupInterfaceGo struct { generator.GoGenerator gv schema.GroupVersion customArgs *args.CustomArgs } func (f *groupInterfaceGo) Imports(context *generator.Context) []string { firstType := f.customArgs.TypesByGroup[f.gv][0] packages := append(Imports, fmt.Sprintf("%s \"%s\"", f.gv.Version, firstType.Package)) return packages } var ( pluralExceptions = map[string]string{ "Endpoints": "Endpoints", } plural = namer.NewPublicPluralNamer(pluralExceptions) ) func (f *groupInterfaceGo) Init(c *generator.Context, w io.Writer) error { sw := generator.NewSnippetWriter(w, c, "{{", "}}") orderer := namer.Orderer{Namer: namer.NewPrivateNamer(0)} var types []*types.Type for _, name := range f.customArgs.TypesByGroup[f.gv] { types = append(types, c.Universe.Type(*name)) } types = orderer.OrderTypes(types) sw.Do("func init() {\n", nil) sw.Do("schemes.Register("+f.gv.Version+".AddToScheme)\n", nil) sw.Do("}\n", nil) sw.Do("type Interface interface {\n", nil) for _, t := range types { m := map[string]interface{}{ "type": t.Name.Name, } sw.Do("{{.type}}() {{.type}}Controller\n", m) } sw.Do("}\n", nil) m := map[string]interface{}{ "version": f.gv.Version, "versionUpper": namer.IC(f.gv.Version), "groupUpper": upperLowercase(f.gv.Group), } sw.Do(groupInterfaceBody, m) for _, t := range types { m := map[string]interface{}{ "type": t.Name.Name, "plural": plural.Name(t), "pluralLower": strings.ToLower(plural.Name(t)), "version": f.gv.Version, "group": f.gv.Group, "namespaced": namespaced(t), "versionUpper": namer.IC(f.gv.Version), "groupUpper": upperLowercase(f.gv.Group), } body := ` func (v *version) {{.type}}() {{.type}}Controller { return generic.New{{ if not .namespaced}}NonNamespaced{{end}}Controller[*{{.version}}.{{.type}}, *{{.version}}.{{.type}}List](schema.GroupVersionKind{Group: "{{.group}}", Version: "{{.version}}", Kind: "{{.type}}"}, "{{.pluralLower}}", {{ if .namespaced}}true, {{end}}v.controllerFactory) } ` sw.Do(body, m) } return sw.Error() } var groupInterfaceBody = ` func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } ` ================================================ FILE: pkg/controller-gen/generators/list_type_go.go ================================================ package generators import ( "io" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/gengo/v2/generator" ) func ListTypesGo(gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Generator { return &listTypesGo{ gv: gv, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: "zz_generated_list_types.go", }, } } type listTypesGo struct { generator.GoGenerator gv schema.GroupVersion customArgs *args.CustomArgs } func (f *listTypesGo) Imports(*generator.Context) []string { return Imports } func (f *listTypesGo) Init(c *generator.Context, w io.Writer) error { sw := generator.NewSnippetWriter(w, c, "{{", "}}") for _, t := range f.customArgs.TypesByGroup[f.gv] { m := map[string]interface{}{ "type": t.Name, } args.CheckType(c.Universe.Type(*t)) sw.Do(string(listTypesBody), m) } return sw.Error() } var listTypesBody = ` // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // {{.type}}List is a list of {{.type}} resources type {{.type}}List struct { metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` metav1.ListMeta ` + "`" + `json:"metadata"` + "`" + ` Items []{{.type}} ` + "`" + `json:"items"` + "`" + ` } func New{{.type}}(namespace, name string, obj {{.type}}) *{{.type}} { obj.APIVersion, obj.Kind = SchemeGroupVersion.WithKind("{{.type}}").ToAPIVersionAndKind() obj.Name = name obj.Namespace = namespace return &obj } ` ================================================ FILE: pkg/controller-gen/generators/register_group_go.go ================================================ package generators import ( "fmt" "strings" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/gengo/v2/generator" ) func RegisterGroupGo(group string, customArgs *args.CustomArgs) generator.Generator { return ®isterGroupGo{ group: group, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: "zz_generated_register.go", }, } } type registerGroupGo struct { generator.GoGenerator group string customArgs *args.CustomArgs } func (f *registerGroupGo) Name() string { // Keep the old behavior of generating comments without the .go suffix return strings.TrimSuffix(f.Filename(), ".go") } func (f *registerGroupGo) PackageConsts(*generator.Context) []string { return []string{ fmt.Sprintf("GroupName = \"%s\"", f.group), } } ================================================ FILE: pkg/controller-gen/generators/register_group_version_go.go ================================================ package generators import ( "fmt" "io" "strings" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "github.com/rancher/wrangler/v3/pkg/name" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" "k8s.io/gengo/v2/types" ) func RegisterGroupVersionGo(gv schema.GroupVersion, customArgs *args.CustomArgs) generator.Generator { return ®isterGroupVersionGo{ gv: gv, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: "zz_generated_register.go", }, } } type registerGroupVersionGo struct { generator.GoGenerator gv schema.GroupVersion customArgs *args.CustomArgs } func (f *registerGroupVersionGo) Imports(*generator.Context) []string { firstType := f.customArgs.TypesByGroup[f.gv][0] typeGroupPath := strings.TrimSuffix(firstType.Package, "/"+f.gv.Version) packages := append(Imports, fmt.Sprintf("%s \"%s\"", groupPath(f.gv.Group), typeGroupPath)) return packages } func (f *registerGroupVersionGo) Init(c *generator.Context, w io.Writer) error { var ( types []*types.Type orderer = namer.Orderer{Namer: namer.NewPrivateNamer(0)} sw = generator.NewSnippetWriter(w, c, "{{", "}}") ) for _, name := range f.customArgs.TypesByGroup[f.gv] { types = append(types, c.Universe.Type(*name)) } types = orderer.OrderTypes(types) m := map[string]interface{}{ "version": f.gv.Version, "groupPath": groupPath(f.gv.Group), } sw.Do("var (\n", nil) for _, t := range types { m := map[string]interface{}{ "name": t.Name.Name + "ResourceName", "plural": name.GuessPluralName(strings.ToLower(t.Name.Name)), } sw.Do("{{.name}} = \"{{.plural}}\"\n", m) } sw.Do(")\n", nil) sw.Do(registerGroupVersionBody, m) for _, t := range types { m := map[string]interface{}{ "type": t.Name.Name, } sw.Do("&{{.type}}{},\n", m) sw.Do("&{{.type}}List{},\n", m) } sw.Do(registerGroupVersionBodyEnd, nil) return sw.Error() } var registerGroupVersionBody = ` // SchemeGroupVersion is group version used to register these objects var SchemeGroupVersion = schema.GroupVersion{Group: {{.groupPath}}.GroupName, Version: "{{.version}}"} // Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) AddToScheme = SchemeBuilder.AddToScheme ) // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, ` var registerGroupVersionBodyEnd = ` ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ` ================================================ FILE: pkg/controller-gen/generators/target.go ================================================ package generators import ( "path/filepath" "strings" args "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/gengo/v2/generator" ) func Target(customArgs *args.CustomArgs, name string, generators func(context *generator.Context) []generator.Generator) generator.SimpleTarget { parts := strings.Split(name, "/") return generator.SimpleTarget{ PkgName: groupPath(parts[len(parts)-1]), PkgPath: name, PkgDir: filepath.Join(customArgs.OutputBase, name), HeaderComment: customArgs.BoilerplateContent, GeneratorsFunc: generators, } } ================================================ FILE: pkg/controller-gen/generators/type_go.go ================================================ package generators import ( "fmt" "io" "strings" "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" "k8s.io/gengo/v2/types" ) func TypeGo(gv schema.GroupVersion, name *types.Name, customArgs *args.CustomArgs) generator.Generator { return &typeGo{ name: name, gv: gv, customArgs: customArgs, GoGenerator: generator.GoGenerator{ OutputFilename: fmt.Sprintf("%s.go", strings.ToLower(name.Name)), }, } } type typeGo struct { generator.GoGenerator name *types.Name gv schema.GroupVersion customArgs *args.CustomArgs } func (f *typeGo) Imports(context *generator.Context) []string { packages := append(Imports, fmt.Sprintf("%s \"%s\"", f.gv.Version, f.name.Package)) return packages } func (f *typeGo) Init(c *generator.Context, w io.Writer) error { sw := generator.NewSnippetWriter(w, c, "{{", "}}") if err := f.GoGenerator.Init(c, w); err != nil { return err } t := c.Universe.Type(*f.name) m := map[string]interface{}{ "type": f.name.Name, "lowerName": namer.IL(f.name.Name), "plural": plural.Name(t), "version": f.gv.Version, "namespaced": namespaced(t), "hasStatus": hasStatus(t), "statusType": statusType(t), } sw.Do(typeBody, m) return sw.Error() } func statusType(t *types.Type) string { for _, m := range t.Members { if m.Name == "Status" { return m.Type.Name.Name } } return "" } func hasStatus(t *types.Type) bool { for _, m := range t.Members { if m.Name == "Status" && m.Type.Name.Package == t.Name.Package { return true } } return false } var typeBody = ` // {{.type}}Controller interface for managing {{.type}} resources. type {{.type}}Controller interface { generic.{{ if not .namespaced}}NonNamespaced{{end}}ControllerInterface[*{{.version}}.{{.type}}, *{{.version}}.{{.type}}List] } // {{.type}}Client interface for managing {{.type}} resources in Kubernetes. type {{.type}}Client interface { generic.{{ if not .namespaced}}NonNamespaced{{end}}ClientInterface[*{{.version}}.{{.type}}, *{{.version}}.{{.type}}List] } // {{.type}}Cache interface for retrieving {{.type}} resources in memory. type {{.type}}Cache interface { generic.{{ if not .namespaced}}NonNamespaced{{end}}CacheInterface[*{{.version}}.{{.type}}] } {{ if .hasStatus -}} // {{.type}}StatusHandler is executed for every added or modified {{.type}}. Should return the new status to be updated type {{.type}}StatusHandler func(obj *{{.version}}.{{.type}}, status {{.version}}.{{.statusType}}) ({{.version}}.{{.statusType}}, error) // {{.type}}GeneratingHandler is the top-level handler that is executed for every {{.type}} event. It extends {{.type}}StatusHandler by a returning a slice of child objects to be passed to apply.Apply type {{.type}}GeneratingHandler func(obj *{{.version}}.{{.type}}, status {{.version}}.{{.statusType}}) ([]runtime.Object, {{.version}}.{{.statusType}}, error) // Register{{.type}}StatusHandler configures a {{.type}}Controller to execute a {{.type}}StatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func Register{{.type}}StatusHandler(ctx context.Context, controller {{.type}}Controller, condition condition.Cond, name string, handler {{.type}}StatusHandler) { statusHandler := &{{.lowerName}}StatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // Register{{.type}}GeneratingHandler configures a {{.type}}Controller to execute a {{.type}}GeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func Register{{.type}}GeneratingHandler(ctx context.Context, controller {{.type}}Controller, apply apply.Apply, condition condition.Cond, name string, handler {{.type}}GeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &{{.lowerName}}GeneratingHandler{ {{.type}}GeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) Register{{.type}}StatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type {{.lowerName}}StatusHandler struct { client {{.type}}Client condition condition.Cond handler {{.type}}StatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *{{.lowerName}}StatusHandler) sync(key string, obj *{{.version}}.{{.type}}) (*{{.version}}.{{.type}}, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type {{.lowerName}}GeneratingHandler struct { {{.type}}GeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *{{.lowerName}}GeneratingHandler) Remove(key string, obj *{{.version}}.{{.type}}) (*{{.version}}.{{.type}}, error) { if obj != nil { return obj, nil } obj = &{{.version}}.{{.type}}{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured {{.type}}GeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *{{.lowerName}}GeneratingHandler) Handle(obj *{{.version}}.{{.type}}, status {{.version}}.{{.statusType}}) ({{.version}}.{{.statusType}}, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.{{.type}}GeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *{{.lowerName}}GeneratingHandler) isNewResourceVersion(obj *{{.version}}.{{.type}}) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *{{.lowerName}}GeneratingHandler) storeResourceVersion(obj *{{.version}}.{{.type}}) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } {{- end }} ` ================================================ FILE: pkg/controller-gen/generators/util.go ================================================ package generators import ( "strings" "k8s.io/code-generator/cmd/client-gen/generators/util" "k8s.io/gengo/v2/namer" "k8s.io/gengo/v2/types" ) var ( Imports = []string{ "context", "sync", "time", "k8s.io/client-go/rest", "github.com/rancher/wrangler/v3/pkg/apply", "github.com/rancher/lasso/pkg/controller", "github.com/rancher/wrangler/v3/pkg/condition", "github.com/rancher/wrangler/v3/pkg/schemes", "github.com/rancher/wrangler/v3/pkg/generic", "github.com/rancher/wrangler/v3/pkg/kv", "k8s.io/apimachinery/pkg/api/equality", "k8s.io/apimachinery/pkg/api/errors", "metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"", "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", "k8s.io/apimachinery/pkg/runtime/schema", "k8s.io/apimachinery/pkg/types", "k8s.io/apimachinery/pkg/watch", } ) func namespaced(t *types.Type) bool { if util.MustParseClientGenTags(t.SecondClosestCommentLines).NonNamespaced { return false } kubeBuilder := false for _, line := range t.SecondClosestCommentLines { if strings.HasPrefix(line, "+kubebuilder:resource:path=") { kubeBuilder = true if strings.Contains(line, "scope=Namespaced") { return true } } } return !kubeBuilder } func groupPath(group string) string { g := strings.Replace(strings.Split(group, ".")[0], "-", "", -1) return groupPackageName(g, "") } func groupPackageName(group, groupPackageName string) string { if groupPackageName != "" { return groupPackageName } if group == "" { return "core" } return group } func upperLowercase(name string) string { return namer.IC(strings.ToLower(groupPath(name))) } ================================================ FILE: pkg/controller-gen/main.go ================================================ package controllergen import ( "fmt" "io" "os" "path/filepath" "sort" "strings" "k8s.io/gengo/args" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/types" cgargs "github.com/rancher/wrangler/v3/pkg/controller-gen/args" "github.com/rancher/wrangler/v3/pkg/controller-gen/generators" "github.com/sirupsen/logrus" "golang.org/x/tools/imports" "k8s.io/apimachinery/pkg/runtime/schema" csargs "k8s.io/code-generator/cmd/client-gen/args" cs "k8s.io/code-generator/cmd/client-gen/generators" types2 "k8s.io/code-generator/cmd/client-gen/types" dpargs "k8s.io/code-generator/cmd/deepcopy-gen/args" dp "k8s.io/code-generator/cmd/deepcopy-gen/generators" infargs "k8s.io/code-generator/cmd/informer-gen/args" inf "k8s.io/code-generator/cmd/informer-gen/generators" lsargs "k8s.io/code-generator/cmd/lister-gen/args" ls "k8s.io/code-generator/cmd/lister-gen/generators" oaargs "k8s.io/kube-openapi/cmd/openapi-gen/args" oa "k8s.io/kube-openapi/pkg/generators" ) func Run(opts cgargs.Options) { genericArgs := args.Default().WithoutDefaultFlagParsing() genericArgs.GoHeaderFilePath = opts.Boilerplate if genericArgs.OutputBase == "./" { //go modules tempDir, err := os.MkdirTemp("", "") if err != nil { return } genericArgs.OutputBase = tempDir defer os.RemoveAll(tempDir) } boilerplate, err := genericArgs.LoadGoBoilerplate() if err != nil { logrus.Fatalf("Loading boilerplate: %v", err) } customArgs := &cgargs.CustomArgs{ ImportPackage: opts.ImportPackage, Options: opts, TypesByGroup: map[schema.GroupVersion][]*types.Name{}, Package: opts.OutputPackage, OutputBase: genericArgs.OutputBase, BoilerplateContent: boilerplate, } inputDirs := parseTypes(customArgs) clientGen := generators.NewClientGenerator() getTargets := func(context *generator.Context) []generator.Target { // replace the default formatter options to ensure unused imports are pruned. // ref: https://github.com/kubernetes/gengo/pull/277#issuecomment-2557462569 goGenerator := generator.NewGoFile() goGenerator.Format = func(src []byte) ([]byte, error) { return imports.Process("", src, nil) } context.FileTypes[generator.GoFileType] = goGenerator return clientGen.GetTargets(context, customArgs) } if err := gengo.Execute( cs.NameSystems(nil), cs.DefaultNameSystem(), getTargets, gengo.StdBuildTag, inputDirs, ); err != nil { logrus.Fatalf("Error: %v", err) } groups := map[string]bool{} listerGroups := map[string]bool{} informerGroups := map[string]bool{} deepCopygroups := map[string]bool{} openAPIGroups := map[string]bool{} for groupName, group := range customArgs.Options.Groups { if group.GenerateTypes { deepCopygroups[groupName] = true } if group.GenerateClients { groups[groupName] = true } if group.GenerateListers { listerGroups[groupName] = true } if group.GenerateInformers { informerGroups[groupName] = true } if group.GenerateOpenAPI { openAPIGroups[groupName] = true } } if len(deepCopygroups) == 0 && len(groups) == 0 && len(listerGroups) == 0 && len(informerGroups) == 0 && len(openAPIGroups) == 0 { if err := copyGoPathToModules(customArgs); err != nil { logrus.Fatalf("go modules copy failed: %v", err) } return } if err := copyGoPathToModules(customArgs); err != nil { logrus.Fatalf("go modules copy failed: %v", err) } if err := generateDeepcopy(deepCopygroups, customArgs); err != nil { logrus.Fatalf("deepcopy failed: %v", err) } if err := generateClientset(groups, customArgs); err != nil { logrus.Fatalf("clientset failed: %v", err) } if err := generateListers(listerGroups, customArgs); err != nil { logrus.Fatalf("listers failed: %v", err) } if err := generateInformers(informerGroups, customArgs); err != nil { logrus.Fatalf("informers failed: %v", err) } if err := generateOpenAPI(openAPIGroups, customArgs); err != nil { logrus.Fatalf("openapi failed: %v", err) } if err := copyGoPathToModules(customArgs); err != nil { logrus.Fatalf("go modules copy failed: %v", err) } } func sourcePackagePath(customArgs *cgargs.CustomArgs, pkgName string) string { pkgSplit := strings.Split(pkgName, string(os.PathSeparator)) pkg := filepath.Join(customArgs.OutputBase, strings.Join(pkgSplit[:3], string(os.PathSeparator))) return pkg } // until k8s code-gen supports gopath func copyGoPathToModules(customArgs *cgargs.CustomArgs) error { pathsToCopy := map[string]bool{} for _, types := range customArgs.TypesByGroup { for _, names := range types { pkg := sourcePackagePath(customArgs, names.Package) pathsToCopy[pkg] = true } } pkg := sourcePackagePath(customArgs, customArgs.Package) pathsToCopy[pkg] = true for pkg := range pathsToCopy { if _, err := os.Stat(pkg); os.IsNotExist(err) { continue } return filepath.Walk(pkg, func(path string, info os.FileInfo, err error) error { newPath := strings.Replace(path, pkg, ".", 1) if info.IsDir() { return os.MkdirAll(newPath, info.Mode()) } return copyFile(path, newPath) }) } return nil } func copyFile(src, dst string) error { var err error var srcfd *os.File var dstfd *os.File var srcinfo os.FileInfo if srcfd, err = os.Open(src); err != nil { return err } defer srcfd.Close() if dstfd, err = os.Create(dst); err != nil { return err } defer dstfd.Close() if _, err = io.Copy(dstfd, srcfd); err != nil { return err } if srcinfo, err = os.Stat(src); err != nil { return err } return os.Chmod(dst, srcinfo.Mode()) } func generateDeepcopy(groups map[string]bool, customArgs *cgargs.CustomArgs) error { if len(groups) == 0 { return nil } deepCopyArgs := dpargs.New() deepCopyArgs.OutputFile = "zz_generated_deepcopy.go" deepCopyArgs.GoHeaderFile = customArgs.Options.Boilerplate inputDirs := []string{} for gv, names := range customArgs.TypesByGroup { if !groups[gv.Group] { continue } inputDirs = append(inputDirs, names[0].Package) deepCopyArgs.BoundingDirs = append(deepCopyArgs.BoundingDirs, names[0].Package) } getTargets := func(context *generator.Context) []generator.Target { return dp.GetTargets(context, deepCopyArgs) } return gengo.Execute( dp.NameSystems(), dp.DefaultNameSystem(), getTargets, gengo.StdBuildTag, inputDirs, ) } func generateClientset(groups map[string]bool, customArgs *cgargs.CustomArgs) error { if len(groups) == 0 { return nil } clientSetArgs := csargs.New() clientSetArgs.ClientsetName = "versioned" clientSetArgs.OutputDir = filepath.Join(customArgs.OutputBase, customArgs.Package, "clientset") clientSetArgs.OutputPkg = filepath.Join(customArgs.Package, "clientset") clientSetArgs.GoHeaderFile = customArgs.Options.Boilerplate var order []schema.GroupVersion for gv := range customArgs.TypesByGroup { if !groups[gv.Group] { continue } order = append(order, gv) } sort.Slice(order, func(i, j int) bool { return order[i].Group < order[j].Group }) inputDirs := []string{} for _, gv := range order { packageName := customArgs.Options.Groups[gv.Group].PackageName if packageName == "" { packageName = gv.Group } names := customArgs.TypesByGroup[gv] inputDirs = append(inputDirs, names[0].Package) clientSetArgs.Groups = append(clientSetArgs.Groups, types2.GroupVersions{ PackageName: packageName, Group: types2.Group(gv.Group), Versions: []types2.PackageVersion{ { Version: types2.Version(gv.Version), Package: names[0].Package, }, }, }) } getTargets := setGenClient(groups, customArgs.TypesByGroup, func(context *generator.Context) []generator.Target { return cs.GetTargets(context, clientSetArgs) }) return gengo.Execute( cs.NameSystems(nil), cs.DefaultNameSystem(), getTargets, gengo.StdBuildTag, inputDirs, ) } func generateOpenAPI(groups map[string]bool, customArgs *cgargs.CustomArgs) error { if len(groups) == 0 { return nil } openAPIArgs := oaargs.New() openAPIArgs.OutputDir = filepath.Join(customArgs.OutputBase, customArgs.Options.OutputPackage, "openapi") openAPIArgs.OutputFile = "zz_generated_openapi.go" openAPIArgs.OutputPkg = customArgs.Options.OutputPackage + "/openapi" openAPIArgs.GoHeaderFile = customArgs.Options.Boilerplate openAPIArgs.OutputModelNameFile = "zz_generated.model_name.go" if err := openAPIArgs.Validate(); err != nil { return err } inputDirsMap := map[string]bool{} inputDirs := []string{} inputModelDirsMap := map[string]bool{} inputModelDirs := []string{} for gv, names := range customArgs.TypesByGroup { if !groups[gv.Group] { continue } group := customArgs.Options.Groups[gv.Group] if _, found := inputDirsMap[names[0].Package]; !found { inputDirsMap[names[0].Package] = true inputDirs = append(inputDirs, names[0].Package) if group.OpenAPIModelPackageName != "" { inputModelDirsMap[names[0].Package] = true inputModelDirs = append(inputModelDirs, names[0].Package) } } for _, dep := range group.OpenAPIDependencies { if _, found := inputDirsMap[dep]; !found { inputDirsMap[dep] = true inputDirs = append(inputDirs, dep) } } } boilerplate, err := gengo.GoBoilerplate(openAPIArgs.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy) if err != nil { return err } getTargets := func(context *generator.Context) []generator.Target { return oa.GetOpenAPITargets(context, openAPIArgs, boilerplate) } if err := gengo.Execute( oa.NameSystems(), oa.DefaultNameSystem(), getTargets, gengo.StdBuildTag, inputDirs, ); err != nil { return err } if len(inputModelDirs) <= 0 { return nil } getModelNameTargets := func(context *generator.Context) []generator.Target { return oa.GetModelNameTargets(context, openAPIArgs, boilerplate) } return gengo.Execute( oa.NameSystems(), oa.DefaultNameSystem(), getModelNameTargets, gengo.StdBuildTag, inputModelDirs, ) } func setGenClient( groups map[string]bool, typesByGroup map[schema.GroupVersion][]*types.Name, f func(*generator.Context) []generator.Target, ) func(*generator.Context) []generator.Target { return func(context *generator.Context) []generator.Target { for gv, names := range typesByGroup { if !groups[gv.Group] { continue } for _, name := range names { var ( p = context.Universe.Package(name.Package) t = p.Type(name.Name) status bool nsed bool kubebuilder bool ) for _, line := range append(t.SecondClosestCommentLines, t.CommentLines...) { switch { case strings.Contains(line, "+kubebuilder:object:root=true"): kubebuilder = true t.SecondClosestCommentLines = append(t.SecondClosestCommentLines, "+genclient") case strings.Contains(line, "+kubebuilder:subresource:status"): status = true case strings.Contains(line, "+kubebuilder:resource:") && strings.Contains(line, "scope=Namespaced"): nsed = true } } if kubebuilder { if !nsed { t.SecondClosestCommentLines = append(t.SecondClosestCommentLines, "+genclient:nonNamespaced") } if !status { t.SecondClosestCommentLines = append(t.SecondClosestCommentLines, "+genclient:noStatus") } foundGroup := false for _, comment := range p.DocComments { if strings.Contains(comment, "+groupName=") { foundGroup = true break } } if !foundGroup { p.DocComments = append(p.DocComments, "+groupName="+gv.Group) p.Comments = append(p.Comments, "+groupName="+gv.Group) fmt.Println(gv.Group, p.DocComments, p.Comments, p.Path) } } } } return f(context) } } func generateInformers(groups map[string]bool, customArgs *cgargs.CustomArgs) error { if len(groups) == 0 { return nil } informerArgs := infargs.New() informerArgs.VersionedClientSetPackage = filepath.Join(customArgs.Package, "clientset/versioned") informerArgs.ListersPackage = filepath.Join(customArgs.Package, "listers") informerArgs.OutputDir = filepath.Join(customArgs.OutputBase, customArgs.Package, "informers") informerArgs.OutputPkg = filepath.Join(customArgs.Package, "informers") informerArgs.GoHeaderFile = customArgs.Options.Boilerplate inputDirs := []string{} for gv, names := range customArgs.TypesByGroup { if !groups[gv.Group] { continue } inputDirs = append(inputDirs, names[0].Package) } getTargets := setGenClient(groups, customArgs.TypesByGroup, func(context *generator.Context) []generator.Target { return inf.GetTargets(context, informerArgs) }) return gengo.Execute( inf.NameSystems(nil), inf.DefaultNameSystem(), getTargets, gengo.StdBuildTag, inputDirs, ) } func generateListers(groups map[string]bool, customArgs *cgargs.CustomArgs) error { if len(groups) == 0 { return nil } listerArgs := lsargs.New() listerArgs.OutputDir = filepath.Join(customArgs.OutputBase, customArgs.Package, "listers") listerArgs.OutputPkg = filepath.Join(customArgs.Package, "listers") listerArgs.GoHeaderFile = customArgs.Options.Boilerplate inputDirs := []string{} for gv, names := range customArgs.TypesByGroup { if !groups[gv.Group] { continue } inputDirs = append(inputDirs, names[0].Package) } getTargets := setGenClient(groups, customArgs.TypesByGroup, func(context *generator.Context) []generator.Target { return ls.GetTargets(context, listerArgs) }) return gengo.Execute( ls.NameSystems(nil), ls.DefaultNameSystem(), getTargets, gengo.StdBuildTag, inputDirs, ) } func parseTypes(customArgs *cgargs.CustomArgs) []string { for groupName, group := range customArgs.Options.Groups { if group.GenerateTypes || group.GenerateClients { customArgs.Options.Groups[groupName] = group } } for groupName, group := range customArgs.Options.Groups { if err := cgargs.ObjectsToGroupVersion(groupName, group.Types, customArgs.TypesByGroup); err != nil { // sorry, should really handle this better panic(err) } } var inputDirs []string for _, names := range customArgs.TypesByGroup { inputDirs = append(inputDirs, names[0].Package) } return inputDirs } ================================================ FILE: pkg/crd/crd.go ================================================ // Package crd handles the dynamic creation and modification of CustomResourceDefinitions. package crd import ( "context" "fmt" "time" "github.com/sirupsen/logrus" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" clientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/wait" ) const waitInterval = 500 * time.Millisecond // BatchCreateCRDs will create or update the list of crds if they do not exist. // It will wait for a specified amount of time for the CRD status to be established before returning an error. // labelSelector can be used to scope the CRDs listed by this function. func BatchCreateCRDs(ctx context.Context, crdClient clientv1.CustomResourceDefinitionInterface, labelSelector labels.Selector, waitDuration time.Duration, crds []*apiextv1.CustomResourceDefinition) error { existingCRDs, err := getExistingCRDs(ctx, crdClient, labelSelector) if err != nil { // we do not need to return this error because we can still attempt to create the CRDs if the list fails logrus.Warnf("unable to list existing CRDs: %s", err.Error()) } // ensure each CRD exist for _, crd := range crds { err = ensureCRD(ctx, crd, crdClient, existingCRDs) if err != nil { return err } } existingCRDs, err = getExistingCRDs(ctx, crdClient, labelSelector) if err != nil { // we do not need to return this error because we can still attempt to create the CRDs if the list fails logrus.Warnf("unable to list existing CRDs: %s", err.Error()) } // ensure each CRD is ready for _, crd := range crds { if existingCRD, ok := existingCRDs[crd.Name]; ok && crdIsReady(existingCRD) { continue } // wait for the CRD to be ready err := waitCRD(ctx, crd.Name, crdClient, waitDuration) if err != nil { return fmt.Errorf("failed waiting on CRD '%s': %w", crd.Name, err) } } return nil } // getExistingCRDs returns a map of all CRD resource on the cluster. func getExistingCRDs(ctx context.Context, crdClient clientv1.CustomResourceDefinitionInterface, labelSelector labels.Selector) (map[string]*apiextv1.CustomResourceDefinition, error) { listOpt := metav1.ListOptions{} if labelSelector != nil { listOpt.LabelSelector = labelSelector.String() } storedCRDs, err := crdClient.List(ctx, listOpt) if err != nil { return nil, fmt.Errorf("failed to list crds: %w", err) } // convert existingCRDs to a map for faster lookup existingCRDs := make(map[string]*apiextv1.CustomResourceDefinition, len(storedCRDs.Items)) for i := range storedCRDs.Items { crd := &storedCRDs.Items[i] existingCRDs[crd.Name] = crd } return existingCRDs, nil } // ensureCRD checks if there is an existing CRD that matches if not it will attempt to create or update the CRD. func ensureCRD(ctx context.Context, crd *apiextv1.CustomResourceDefinition, crdClient clientv1.CustomResourceDefinitionInterface, existingCRDs map[string]*apiextv1.CustomResourceDefinition) error { existingCRD, exist := existingCRDs[crd.Name] if !exist { logrus.Infof("Creating embedded CRD %s", crd.Name) _, err := crdClient.Create(ctx, crd, metav1.CreateOptions{}) if err == nil { return nil } if !apierrors.IsAlreadyExists(err) { return fmt.Errorf("failed to create crd '%s': %w", crd.Name, err) } // item was not in initial list but does exist so we will attempt to get the latest resourceVersion existingCRD, err = crdClient.Get(ctx, crd.Name, metav1.GetOptions{}) if err != nil { return fmt.Errorf("failed to get exiting '%s' CRD: %w", crd.Name, err) } // fallthrough and attempt an update } // only keep the resource version for the desired object crd.ResourceVersion = existingCRD.ResourceVersion // if the CRD exists attempt to update it logrus.Infof("Updating embedded CRD %s", crd.Name) _, err := crdClient.Update(ctx, crd, metav1.UpdateOptions{}) if err != nil { return fmt.Errorf("failed to update crd '%s': %w", crd.Name, err) } return nil } // waitCRD repeatably checks CRD status until it is established. func waitCRD(ctx context.Context, crdName string, crdClient clientv1.CustomResourceDefinitionInterface, waitDuration time.Duration) error { logrus.Infof("Waiting for CRD %s to become available", crdName) defer logrus.Infof("Done waiting for CRD %s to become available", crdName) return wait.PollImmediate(waitInterval, waitDuration, func() (bool, error) { crd, err := crdClient.Get(ctx, crdName, metav1.GetOptions{}) if err != nil { return false, fmt.Errorf("failed to get CRD '%s' for status checking: %w", crdName, err) } if crdIsReady(crd) { return true, nil } return false, nil }) } func crdIsReady(crd *apiextv1.CustomResourceDefinition) bool { for i := range crd.Status.Conditions { cond := &crd.Status.Conditions[i] switch cond.Type { case apiextv1.Established: if cond.Status == apiextv1.ConditionTrue { return true } case apiextv1.NamesAccepted: if cond.Status == apiextv1.ConditionFalse { logrus.Infof("Name conflict for CRD %s: %s", crd.Name, cond.Reason) } } } return false } ================================================ FILE: pkg/crd/crd_test.go ================================================ package crd //go:generate mockgen --build_flags=--mod=mod -package crd -destination ./mockCRDClient_test.go "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" CustomResourceDefinitionInterface import ( "context" "errors" "reflect" "testing" "time" "go.uber.org/mock/gomock" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" 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" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation/field" ) var errTest = errors.New("test Error") func TestBatchCreateCRDs(t *testing.T) { t.Parallel() // decrease the ready wait duration for tests waitDuration := time.Second * 5 // create 3 CRDs no status clone and update status in setup crd1 := &apiextv1.CustomResourceDefinition{} crd1.Name = "crd1s.testGroup" crd1.Spec.Group = "testGroup" crd1.Spec.Names.Plural = "crd1s" crd1.Spec.Names.Kind = "CRD1" crd1.Spec.Scope = apiextv1.ClusterScoped crd1.Spec.Versions = []apiextv1.CustomResourceDefinitionVersion{{Name: "v1"}} crd2 := &apiextv1.CustomResourceDefinition{} crd2.Name = "crd2s.testGroup" crd2.Spec.Group = "testGroup" crd2.Spec.Names.Plural = "crd2s" crd2.Spec.Names.Kind = "CRD2" crd2.Spec.Scope = apiextv1.ClusterScoped crd2.Spec.Versions = []apiextv1.CustomResourceDefinitionVersion{{Name: "v1"}} crd3 := &apiextv1.CustomResourceDefinition{} crd3.Name = "crd3s.testGroup" crd3.Spec.Group = "testGroup" crd3.Spec.Names.Plural = "crd3s" crd3.Spec.Names.Kind = "CRD3" crd3.Spec.Scope = apiextv1.ClusterScoped crd3.Spec.Versions = []apiextv1.CustomResourceDefinitionVersion{{Name: "v1"}} tests := []struct { name string toCreateCRDs []*apiextv1.CustomResourceDefinition selector labels.Selector wantErr bool setupMock func(*MockCustomResourceDefinitionInterface) }{ { name: "create single CRD", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1}, selector: labels.Nothing(), setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: labels.Nothing().String()}).Return(list, nil) mock.EXPECT().Create(gomock.Any(), crd1, gomock.Any()) readyCRD := crd1.DeepCopy() readyCRD.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list2 := &apiextv1.CustomResourceDefinitionList{Items: []apiextv1.CustomResourceDefinition{*readyCRD}} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) }, }, { name: "create multiple CRDs", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1, crd2, crd3}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { // initial list should be allowed to fail. mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(nil, errTest) mock.EXPECT().Create(gomock.Any(), crd1, gomock.Any()) mock.EXPECT().Create(gomock.Any(), crd2, gomock.Any()) mock.EXPECT().Create(gomock.Any(), crd3, gomock.Any()) readyCRD1 := crd1.DeepCopy() readyCRD1.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD2 := crd2.DeepCopy() readyCRD2.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD3 := crd3.DeepCopy() readyCRD3.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list2 := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*readyCRD1, *readyCRD2, *readyCRD3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) }, }, { name: "create already exist CRD", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Create(gomock.Any(), crd1, gomock.Any()).Return(nil, apierrors.NewAlreadyExists(apiextv1.Resource("customeresourcedefinitions"), crd1.Name)) mock.EXPECT().Get(gomock.Any(), crd1.Name, gomock.Any()).Return(crd1, nil) mock.EXPECT().Update(gomock.Any(), crd1, gomock.Any()) readyCRD := crd1.DeepCopy() readyCRD.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list2 := &apiextv1.CustomResourceDefinitionList{Items: []apiextv1.CustomResourceDefinition{*readyCRD}} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) }, }, { name: "create failed", wantErr: true, toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Create(gomock.Any(), crd1, gomock.Any()).Return(nil, errTest) }, }, { name: "update CRD", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{Items: []apiextv1.CustomResourceDefinition{*crd1}} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Update(gomock.Any(), crd1, gomock.Any()) readyCRD := crd1.DeepCopy() readyCRD.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list2 := &apiextv1.CustomResourceDefinitionList{Items: []apiextv1.CustomResourceDefinition{*readyCRD}} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) }, }, { name: "update Failed", wantErr: true, toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{Items: []apiextv1.CustomResourceDefinition{*crd1}} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Update(gomock.Any(), crd1, gomock.Any()).Return(nil, errTest) }, }, { name: "update multiple CRDs", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1, crd2, crd3}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*crd1, *crd2, *crd3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Update(gomock.Any(), crd1, gomock.Any()) mock.EXPECT().Update(gomock.Any(), crd2, gomock.Any()) mock.EXPECT().Update(gomock.Any(), crd3, gomock.Any()) readyCRD1 := crd1.DeepCopy() readyCRD1.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD2 := crd2.DeepCopy() readyCRD2.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD3 := crd3.DeepCopy() readyCRD3.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list2 := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*readyCRD1, *readyCRD2, *readyCRD3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) }, }, { name: "create and update Multiple CRDs", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1, crd2, crd3}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*crd2, *crd3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Create(gomock.Any(), crd1, gomock.Any()) mock.EXPECT().Update(gomock.Any(), crd2, gomock.Any()) mock.EXPECT().Update(gomock.Any(), crd3, gomock.Any()) readyCRD1 := crd1.DeepCopy() readyCRD1.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD2 := crd2.DeepCopy() readyCRD2.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD3 := crd3.DeepCopy() readyCRD3.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list2 := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*readyCRD1, *readyCRD2, *readyCRD3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) }, }, { name: "wait for Multiple CRDs", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1, crd2, crd3}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { readyCRD1 := crd1.DeepCopy() readyCRD1.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD2 := crd2.DeepCopy() readyCRD2.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } readyCRD3 := crd3.DeepCopy() readyCRD3.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionTrue}, } list := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*readyCRD1, *readyCRD2, *readyCRD3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Update(gomock.Any(), crd1, gomock.Any()) mock.EXPECT().Update(gomock.Any(), crd2, gomock.Any()) mock.EXPECT().Update(gomock.Any(), crd3, gomock.Any()) notReadyCRD1 := crd1.DeepCopy() notReadyCRD1.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionFalse}, } notReadyCRD2 := crd2.DeepCopy() notReadyCRD2.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionFalse}, } notReadyCRD3 := crd3.DeepCopy() notReadyCRD3.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionFalse}, } list2 := &apiextv1.CustomResourceDefinitionList{ Items: []apiextv1.CustomResourceDefinition{*readyCRD1, *readyCRD2, *readyCRD3}, } mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) mock.EXPECT().Get(gomock.Any(), crd1.Name, gomock.Any()).Return(readyCRD1, nil).AnyTimes() mock.EXPECT().Get(gomock.Any(), crd1.Name, gomock.Any()).Return(readyCRD2, nil).AnyTimes() mock.EXPECT().Get(gomock.Any(), crd1.Name, gomock.Any()).Return(readyCRD3, nil).AnyTimes() }, }, { name: "wait for CRD that doesn't resolve", toCreateCRDs: []*apiextv1.CustomResourceDefinition{crd1}, setupMock: func(mock *MockCustomResourceDefinitionInterface) { list := &apiextv1.CustomResourceDefinitionList{} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list, nil) mock.EXPECT().Create(gomock.Any(), crd1, gomock.Any()) notReadyCRD := crd1.DeepCopy() notReadyCRD.Status.Conditions = []apiextv1.CustomResourceDefinitionCondition{ {Type: apiextv1.Established, Status: apiextv1.ConditionFalse}, } list2 := &apiextv1.CustomResourceDefinitionList{Items: []apiextv1.CustomResourceDefinition{*notReadyCRD}} mock.EXPECT().List(gomock.Any(), metav1.ListOptions{}).Return(list2, nil) mock.EXPECT().Get(gomock.Any(), crd1.Name, gomock.Any()).Return(notReadyCRD, nil).AnyTimes() }, wantErr: true, }, } for i := range tests { tt := &tests[i] t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) mock := NewMockCustomResourceDefinitionInterface(ctrl) if tt.setupMock != nil { tt.setupMock(mock) } if err := BatchCreateCRDs(context.Background(), mock, tt.selector, waitDuration, tt.toCreateCRDs); (err != nil) != tt.wantErr { t.Errorf("BatchCreateCRDs() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestCreateCRDWithColumns(t *testing.T) { tests := []struct { name string wantErr bool columns []apiextv1.CustomResourceColumnDefinition crd func() CRD }{ { name: "Basic CRD with no printer column", wantErr: false, crd: func() CRD { type ExampleSpec struct { Source string `json:"source,omitempty"` Checksum string `json:"checksum,omitempty"` } type Example struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ExampleSpec `json:"spec,omitempty"` } example := Example{} return NamespacedType("Example.example.com/v1").WithSchemaFromStruct(example).WithColumnsFromStruct(example) }, }, { name: "Basic CRD with single printer column", wantErr: false, columns: []apiextv1.CustomResourceColumnDefinition{ {Name: "Source", Type: "string", Format: "", Description: "", Priority: 0, JSONPath: ".spec.source"}, }, crd: func() CRD { type ExampleSpec struct { Source string `json:"source,omitempty" column:""` Checksum string `json:"checksum,omitempty"` } type Example struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ExampleSpec `json:"spec,omitempty"` } example := Example{} return NamespacedType("Example.example.com/v1").WithSchemaFromStruct(example).WithColumnsFromStruct(example) }, }, { name: "Basic CRD with single printer column and custom name", wantErr: false, columns: []apiextv1.CustomResourceColumnDefinition{ {Name: "ExampleSource", Type: "string", Format: "", Description: "", Priority: 0, JSONPath: ".spec.source"}, }, crd: func() CRD { type ExampleSpec struct { Source string `json:"source,omitempty" column:"name=ExampleSource"` Checksum string `json:"checksum,omitempty"` } type Example struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ExampleSpec `json:"spec,omitempty"` } example := Example{} return NamespacedType("Example.example.com/v1").WithSchemaFromStruct(example).WithColumnsFromStruct(example) }, }, { name: "Basic CRD with struct field columns", wantErr: false, columns: []apiextv1.CustomResourceColumnDefinition{ {Name: "Time", Type: "string", Format: "date-time", Description: "", Priority: 0, JSONPath: ".spec.time"}, {Name: "Quantity", Type: "string", Format: "", Description: "", Priority: 0, JSONPath: ".spec.quantity"}, }, crd: func() CRD { type ExampleSpec struct { Time *metav1.Time `json:"time,omitempty" column:""` Quantity *resource.Quantity `json:"quantity,omitempty" column:""` Checksum string `json:"checksum,omitempty"` } type Example struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ExampleSpec `json:"spec,omitempty"` } example := Example{} return NamespacedType("Example.example.com/v1").WithSchemaFromStruct(example).WithColumnsFromStruct(example) }, }, { name: "Complex CRD with mix of struct and basic field columns", wantErr: false, columns: []apiextv1.CustomResourceColumnDefinition{ {Name: "Time", Type: "string", Format: "date-time", Description: "", Priority: 0, JSONPath: ".spec.time"}, {Name: "Quantity", Type: "string", Format: "", Description: "", Priority: 0, JSONPath: ".spec.quantity"}, {Name: "Byte", Type: "string", Format: "byte", Description: "", Priority: 0, JSONPath: ".status.checksum"}, {Name: "Password", Type: "string", Format: "password", Description: "", Priority: 0, JSONPath: ".status.password"}, {Name: "Boolean", Type: "boolean", Format: "", Description: "", Priority: 0, JSONPath: ".status.boolean"}, {Name: "Float", Type: "number", Format: "", Description: "", Priority: 0, JSONPath: ".status.float"}, {Name: "Integer", Type: "integer", Format: "", Description: "", Priority: 0, JSONPath: ".status.integer"}, {Name: "IntOrString", Type: "string", Format: "", Description: "", Priority: 0, JSONPath: ".status.intOrString"}, }, crd: func() CRD { type ExampleSpec struct { Time *metav1.Time `json:"time,omitempty" column:""` Quantity *resource.Quantity `json:"quantity,omitempty" column:""` } type ExampleStatus struct { Byte string `json:"checksum,omitempty" column:"format=byte"` Password string `json:"password,omitempty" column:"format=password"` Boolean *bool `json:"boolean,omitempty" column:""` Float *float32 `json:"float,omitempty" column:""` Integer *int32 `json:"integer,omitempty" column:""` IntOrString *intstr.IntOrString `json:"intOrString,omitempty" column:""` } type Example struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ExampleSpec `json:"spec,omitempty"` Status ExampleStatus `json:"status,omitempty"` } example := Example{} return NamespacedType("Example.example.com/v1").WithSchemaFromStruct(example).WithColumnsFromStruct(example) }, }, } for i := range tests { tt := &tests[i] t.Run(tt.name, func(t *testing.T) { o, err := tt.crd().ToCustomResourceDefinition() if (err != nil) != tt.wantErr { t.Fatalf("ToCustomResourceDefinition() error = %v, wantErr %v", err, tt.wantErr) } unstructuredCRD, ok := o.(*unstructured.Unstructured) if !ok { t.Fatal("could not convert CRD runtime.Object to *unstructured.Unstructured") } var v1CRD *apiextv1.CustomResourceDefinition if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredCRD.UnstructuredContent(), &v1CRD); err != nil { t.Fatalf("Failed to convert CRD *unstructured.Unstructured to *apiextv1.CustomResourceDefinition: %v", err) } if len(v1CRD.Spec.Versions) == 0 { t.Errorf("CRD has no schema versions") } fldPath := field.NewPath("spec") for _, version := range v1CRD.Spec.Versions { for i := range version.AdditionalPrinterColumns { apc := &apiextensions.CustomResourceColumnDefinition{} if err := apiextv1.Convert_v1_CustomResourceColumnDefinition_To_apiextensions_CustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], apc, nil); err != nil { t.Errorf("Failed to convert apiextv1.CustomResourceColumnDefinition to apiextensions.CustomResourceColumnDefinition for validation: %v", err) } if errs := validation.ValidateCustomResourceColumnDefinition(apc, fldPath.Child("additionalPrinterColumns").Index(i)); len(errs) > 0 { t.Errorf("AdditionalPrinterColumn definition validation failed: %s", errs.ToAggregate().Error()) } } if !reflect.DeepEqual(tt.columns, version.AdditionalPrinterColumns) { t.Errorf("AdditionalPrinterColumns = %#v,\n\t\twanted columns = %#v", version.AdditionalPrinterColumns, tt.columns) } } }) } } ================================================ FILE: pkg/crd/init.go ================================================ package crd import ( "context" "fmt" "path/filepath" "reflect" "strconv" "strings" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/kv" "github.com/rancher/wrangler/v3/pkg/name" "github.com/rancher/wrangler/v3/pkg/schemas/openapi" "github.com/sirupsen/logrus" apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/rest" // Ensure the gvks are loaded so that apply works correctly _ "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1" ) var ( // Ref: https://github.com/kubernetes/apiextensions-apiserver/blob/v0.28.0/pkg/apis/apiextensions/validation/validation.go#L53 customResourceColumnDefinitionFormats = sets.NewString("int32", "int64", "float", "double", "byte", "date", "date-time", "password") ) const CRDKind = "CustomResourceDefinition" type Factory struct { wg sync.WaitGroup err error CRDClient clientset.Interface apply apply.Apply } // CRD defines information about a CRD that can be used to create a CustomResourceDefinition runtime.Object // Deprecated: Rancher does not plan to continue support for dynamically defined CRDs type CRD struct { GVK schema.GroupVersionKind PluralName string SingularName string NonNamespace bool Schema *apiextv1.JSONSchemaProps SchemaObject interface{} Columns []apiextv1.CustomResourceColumnDefinition Status bool Scale bool Categories []string ShortNames []string Labels map[string]string Annotations map[string]string Override runtime.Object } func (c CRD) WithSchema(schema *apiextv1.JSONSchemaProps) CRD { c.Schema = schema return c } func (c CRD) WithSchemaFromStruct(obj interface{}) CRD { c.SchemaObject = obj return c } func (c CRD) WithColumn(name, path string) CRD { c.Columns = append(c.Columns, apiextv1.CustomResourceColumnDefinition{ Name: name, Type: "string", Priority: 0, JSONPath: path, }) return c } func getType(obj interface{}) reflect.Type { if t, ok := obj.(reflect.Type); ok { return t } t := reflect.TypeOf(obj) if t.Kind() == reflect.Ptr { t = t.Elem() } return t } func (c CRD) WithColumnsFromStruct(obj interface{}) CRD { c.Columns = append(c.Columns, readCustomColumns(getType(obj), ".")...) return c } func fieldName(f reflect.StructField) string { jsonTag := f.Tag.Get("json") if jsonTag == "-" { return "" } name := strings.Split(jsonTag, ",")[0] if name == "" { return f.Name } return name } func tagToColumn(f reflect.StructField, kind reflect.Kind, format, path string) (apiextv1.CustomResourceColumnDefinition, bool) { // Column definitions support only a subset of the full OpenAPI schema formats if !customResourceColumnDefinitionFormats.Has(format) { format = "" } c := apiextv1.CustomResourceColumnDefinition{ Name: f.Name, Type: kindToType(kind), Format: format, JSONPath: path, } columnDef, ok := f.Tag.Lookup("column") if !ok { return c, false } for k, v := range kv.SplitMap(columnDef, ",") { switch k { case "name": c.Name = v case "type": c.Type = v case "format": c.Format = v case "description": c.Description = v case "priority": p, _ := strconv.Atoi(v) c.Priority = int32(p) case "jsonpath": c.JSONPath = v } } return c, true } func kindToType(k reflect.Kind) string { // Ref: https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/endpoints/installer.go#L1178 switch s := k.String(); s { case "bool", "*bool": return "boolean" case "uint8", "*uint8", "int", "*int", "int32", "*int32", "int64", "*int64", "uint32", "*uint32", "uint64", "*uint64": return "integer" case "float64", "*float64", "float32", "*float32": return "number" case "byte", "*byte": return "string" case "[]string", "[]*string": return "string" case "[]int32", "[]*int32": return "integer" default: return s } } type openAPISchemaProvider interface { OpenAPISchemaType() []string OpenAPISchemaFormat() string } func openAPISchema(t reflect.Type) (reflect.Kind, string) { var format string if o, ok := reflect.New(t).Interface().(openAPISchemaProvider); ok { format = o.OpenAPISchemaFormat() if st := o.OpenAPISchemaType(); len(st) > 0 { switch st[0] { case "string": return reflect.String, format } } } return reflect.Invalid, format } func readCustomColumns(t reflect.Type, path string) (result []apiextv1.CustomResourceColumnDefinition) { for i := 0; i < t.NumField(); i++ { f := t.Field(i) fieldName := fieldName(f) if fieldName == "" { continue } t := f.Type if t.Kind() == reflect.Ptr { t = t.Elem() } if kind, format := openAPISchema(t); kind != reflect.Invalid { if col, ok := tagToColumn(f, kind, format, path+fieldName); ok { result = append(result, col) } } else if t.Kind() == reflect.Struct { if f.Anonymous { result = append(result, readCustomColumns(t, path)...) } else { result = append(result, readCustomColumns(t, path+fieldName+".")...) } } else { if col, ok := tagToColumn(f, t.Kind(), "", path+fieldName); ok { result = append(result, col) } } } return result } func (c CRD) WithCustomColumn(columns ...apiextv1.CustomResourceColumnDefinition) CRD { c.Columns = append(c.Columns, columns...) return c } func (c CRD) WithStatus() CRD { c.Status = true return c } func (c CRD) WithScale() CRD { c.Scale = true return c } func (c CRD) WithCategories(categories ...string) CRD { c.Categories = categories return c } func (c CRD) WithGroup(group string) CRD { c.GVK.Group = group return c } func (c CRD) WithShortNames(shortNames ...string) CRD { c.ShortNames = shortNames return c } func (c CRD) ToCustomResourceDefinition() (runtime.Object, error) { if c.Override != nil { return c.Override, nil } if c.SchemaObject != nil && c.GVK.Kind == "" { t := getType(c.SchemaObject) c.GVK.Kind = t.Name() } if c.SchemaObject != nil && c.GVK.Version == "" { t := getType(c.SchemaObject) c.GVK.Version = filepath.Base(t.PkgPath()) } if c.SchemaObject != nil && c.GVK.Group == "" { t := getType(c.SchemaObject) c.GVK.Group = filepath.Base(filepath.Dir(t.PkgPath())) } plural := c.PluralName if plural == "" { plural = strings.ToLower(name.GuessPluralName(c.GVK.Kind)) } singular := c.SingularName if singular == "" { singular = strings.ToLower(c.GVK.Kind) } name := c.Name() crd := apiextv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: apiextv1.CustomResourceDefinitionSpec{ Group: c.GVK.Group, Versions: []apiextv1.CustomResourceDefinitionVersion{ { Name: c.GVK.Version, Storage: true, Served: true, AdditionalPrinterColumns: c.Columns, }, }, Names: apiextv1.CustomResourceDefinitionNames{ Plural: plural, Singular: singular, Kind: c.GVK.Kind, Categories: c.Categories, ShortNames: c.ShortNames, }, PreserveUnknownFields: false, }, } if c.Schema != nil { crd.Spec.Versions[0].Schema = &apiextv1.CustomResourceValidation{ OpenAPIV3Schema: c.Schema, } } if c.SchemaObject != nil { schema, err := openapi.ToOpenAPIFromStruct(c.SchemaObject) if err != nil { return nil, err } crd.Spec.Versions[0].Schema = &apiextv1.CustomResourceValidation{ OpenAPIV3Schema: schema, } } // add a dummy schema because v1 requires OpenAPIV3Schema to be set if crd.Spec.Versions[0].Schema == nil { crd.Spec.Versions[0].Schema = &apiextv1.CustomResourceValidation{ OpenAPIV3Schema: &apiextv1.JSONSchemaProps{ Type: "object", Properties: map[string]apiextv1.JSONSchemaProps{ "spec": { XPreserveUnknownFields: &[]bool{true}[0], }, "status": { XPreserveUnknownFields: &[]bool{true}[0], }, }, }, } } if c.Status { crd.Spec.Versions[0].Subresources = &apiextv1.CustomResourceSubresources{ Status: &apiextv1.CustomResourceSubresourceStatus{}, } if c.Scale { sel := "Spec.Selector" crd.Spec.Versions[0].Subresources.Scale = &apiextv1.CustomResourceSubresourceScale{ SpecReplicasPath: "Spec.Replicas", StatusReplicasPath: "Status.Replicas", LabelSelectorPath: &sel, } } } if c.NonNamespace { crd.Spec.Scope = apiextv1.ClusterScoped } else { crd.Spec.Scope = apiextv1.NamespaceScoped } crd.Labels = c.Labels crd.Annotations = c.Annotations // Convert to unstructured to ensure that PreserveUnknownFields=false is set because the struct will omit false mapData, err := convert.EncodeToMap(crd) if err != nil { return nil, err } mapData["kind"] = CRDKind mapData["apiVersion"] = apiextv1.SchemeGroupVersion.String() return &unstructured.Unstructured{ Object: mapData, }, unstructured.SetNestedField(mapData, false, "spec", "preserveUnknownFields") } func (c CRD) ToCustomResourceDefinitionV1Beta1() (*apiextv1beta1.CustomResourceDefinition, error) { toConvertCRD, err := c.ToCustomResourceDefinition() if err != nil { return nil, err } if toConvertCRD == nil { return nil, fmt.Errorf("cannot convert empty CRD runtime object to apiextensions v1beta1 CRD object") } unstructuredCRD, ok := toConvertCRD.(*unstructured.Unstructured) if !ok { return nil, fmt.Errorf("could not convert CRD runtime object to *unstructured.Unstructured") } var v1CRD *apiextv1.CustomResourceDefinition if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredCRD.UnstructuredContent(), &v1CRD); err != nil { return nil, err } internalCRD := &apiext.CustomResourceDefinition{} if err := apiextv1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(v1CRD, internalCRD, nil); err != nil { return nil, err } v1beta1CRD := &apiextv1beta1.CustomResourceDefinition{} if err := apiextv1beta1.Convert_apiextensions_CustomResourceDefinition_To_v1beta1_CustomResourceDefinition(internalCRD, v1beta1CRD, nil); err != nil { return nil, err } // GVK is dropped during conversion, so we must add it. v1beta1CRD.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{ Group: apiextv1beta1.SchemeGroupVersion.Group, Version: apiextv1beta1.SchemeGroupVersion.Version, Kind: CRDKind, }) return v1beta1CRD, nil } // Name resolves the Name for the given CRD. func (c *CRD) Name() string { if meta, ok := c.Override.(metav1.Object); ok && c.Override != nil { return meta.GetName() } kind := c.GVK.Kind if c.SchemaObject != nil && kind == "" { t := getType(c.SchemaObject) kind = t.Name() } group := c.GVK.Group if c.SchemaObject != nil && group == "" { t := getType(c.SchemaObject) group = filepath.Base(filepath.Dir(t.PkgPath())) } plural := c.PluralName if plural == "" { plural = strings.ToLower(name.GuessPluralName(kind)) } return strings.ToLower(plural + "." + group) } func NamespacedType(name string) CRD { kindGroup, version := kv.Split(name, "/") kind, group := kv.Split(kindGroup, ".") kind = convert.Capitalize(kind) group = strings.ToLower(group) return FromGV(schema.GroupVersion{ Group: group, Version: version, }, kind) } func New(group, version string) CRD { return CRD{ GVK: schema.GroupVersionKind{ Group: group, Version: version, }, PluralName: "", NonNamespace: false, Schema: nil, SchemaObject: nil, Columns: nil, Status: false, Scale: false, Categories: nil, ShortNames: nil, } } func NamespacedTypes(names ...string) (ret []CRD) { for _, name := range names { ret = append(ret, NamespacedType(name)) } return } func NonNamespacedType(name string) CRD { crd := NamespacedType(name) crd.NonNamespace = true return crd } func NonNamespacedTypes(names ...string) (ret []CRD) { for _, name := range names { ret = append(ret, NonNamespacedType(name)) } return } func FromGV(gv schema.GroupVersion, kind string) CRD { return CRD{ GVK: gv.WithKind(kind), } } func NewFactoryFromClient(config *rest.Config) (*Factory, error) { apply, err := apply.NewForConfig(config) if err != nil { return nil, err } f, err := clientset.NewForConfig(config) if err != nil { return nil, err } return &Factory{ CRDClient: f, apply: apply.WithDynamicLookup().WithNoDelete(), }, nil } func (f *Factory) BatchWait() error { f.wg.Wait() return f.err } func (f *Factory) BatchCreateCRDs(ctx context.Context, crds ...CRD) *Factory { f.wg.Add(1) go func() { defer f.wg.Done() if _, err := f.CreateCRDs(ctx, crds...); err != nil && f.err == nil { f.err = err } }() return f } func (f *Factory) CreateCRDs(ctx context.Context, crds ...CRD) (map[schema.GroupVersionKind]*apiextv1.CustomResourceDefinition, error) { if len(crds) == 0 { return nil, nil } if ok, err := f.ensureAccess(ctx); err != nil { return nil, err } else if !ok { logrus.Infof("No access to list CRDs, assuming CRDs are pre-created.") return nil, err } crdStatus := map[schema.GroupVersionKind]*apiextv1.CustomResourceDefinition{} ready, err := f.getReadyCRDs(ctx) if err != nil { return nil, err } for _, crdDef := range crds { crd, err := f.createCRD(ctx, crdDef, ready) if err != nil { return nil, err } crdStatus[crdDef.GVK] = crd } ready, err = f.getReadyCRDs(ctx) if err != nil { return nil, err } for gvk, crd := range crdStatus { if readyCrd, ok := ready[crd.Name]; ok { crdStatus[gvk] = readyCrd } else { if err := f.waitCRD(ctx, crd.Name, gvk, crdStatus); err != nil { return nil, err } } } return crdStatus, nil } func (f *Factory) waitCRD(ctx context.Context, crdName string, gvk schema.GroupVersionKind, crdStatus map[schema.GroupVersionKind]*apiextv1.CustomResourceDefinition) error { logrus.Infof("Waiting for CRD %s to become available", crdName) defer logrus.Infof("Done waiting for CRD %s to become available", crdName) first := true return wait.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) { if !first { logrus.Infof("Waiting for CRD %s to become available", crdName) } first = false crd, err := f.CRDClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crdName, metav1.GetOptions{}) if err != nil { return false, err } for _, cond := range crd.Status.Conditions { switch cond.Type { case apiextv1.Established: if cond.Status == apiextv1.ConditionTrue { crdStatus[gvk] = crd return true, err } case apiextv1.NamesAccepted: if cond.Status == apiextv1.ConditionFalse { logrus.Infof("Name conflict on %s: %v\n", crdName, cond.Reason) } } } return false, ctx.Err() }) } func (f *Factory) createCRD(ctx context.Context, crdDef CRD, ready map[string]*apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, error) { crd, err := crdDef.ToCustomResourceDefinition() if err != nil { return nil, err } meta, err := meta.Accessor(crd) if err != nil { return nil, err } logrus.Infof("Applying CRD %s", meta.GetName()) if err := f.apply.WithOwner(crd).ApplyObjects(crd); err != nil { return nil, err } return f.CRDClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, meta.GetName(), metav1.GetOptions{}) } func (f *Factory) ensureAccess(ctx context.Context) (bool, error) { _, err := f.CRDClient.ApiextensionsV1().CustomResourceDefinitions().List(ctx, metav1.ListOptions{}) if apierrors.IsForbidden(err) { return false, nil } return true, err } func (f *Factory) getReadyCRDs(ctx context.Context) (map[string]*apiextv1.CustomResourceDefinition, error) { list, err := f.CRDClient.ApiextensionsV1().CustomResourceDefinitions().List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } result := map[string]*apiextv1.CustomResourceDefinition{} for i, crd := range list.Items { for _, cond := range crd.Status.Conditions { switch cond.Type { case apiextv1.Established: if cond.Status == apiextv1.ConditionTrue { result[crd.Name] = &list.Items[i] } } } } return result, nil } ================================================ FILE: pkg/crd/mockCRDClient_test.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1 (interfaces: CustomResourceDefinitionInterface) // // Generated by this command: // // mockgen --build_flags=--mod=mod -package crd -destination ./mockCRDClient_test.go k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1 CustomResourceDefinitionInterface // // Package crd is a generated GoMock package. package crd import ( context "context" reflect "reflect" gomock "go.uber.org/mock/gomock" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" v10 "k8s.io/apiextensions-apiserver/pkg/client/applyconfiguration/apiextensions/v1" v11 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" ) // MockCustomResourceDefinitionInterface is a mock of CustomResourceDefinitionInterface interface. type MockCustomResourceDefinitionInterface struct { ctrl *gomock.Controller recorder *MockCustomResourceDefinitionInterfaceMockRecorder isgomock struct{} } // MockCustomResourceDefinitionInterfaceMockRecorder is the mock recorder for MockCustomResourceDefinitionInterface. type MockCustomResourceDefinitionInterfaceMockRecorder struct { mock *MockCustomResourceDefinitionInterface } // NewMockCustomResourceDefinitionInterface creates a new mock instance. func NewMockCustomResourceDefinitionInterface(ctrl *gomock.Controller) *MockCustomResourceDefinitionInterface { mock := &MockCustomResourceDefinitionInterface{ctrl: ctrl} mock.recorder = &MockCustomResourceDefinitionInterfaceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockCustomResourceDefinitionInterface) EXPECT() *MockCustomResourceDefinitionInterfaceMockRecorder { return m.recorder } // Apply mocks base method. func (m *MockCustomResourceDefinitionInterface) Apply(ctx context.Context, customResourceDefinition *v10.CustomResourceDefinitionApplyConfiguration, opts v11.ApplyOptions) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Apply", ctx, customResourceDefinition, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // Apply indicates an expected call of Apply. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Apply(ctx, customResourceDefinition, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Apply), ctx, customResourceDefinition, opts) } // ApplyStatus mocks base method. func (m *MockCustomResourceDefinitionInterface) ApplyStatus(ctx context.Context, customResourceDefinition *v10.CustomResourceDefinitionApplyConfiguration, opts v11.ApplyOptions) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ApplyStatus", ctx, customResourceDefinition, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // ApplyStatus indicates an expected call of ApplyStatus. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) ApplyStatus(ctx, customResourceDefinition, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyStatus", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).ApplyStatus), ctx, customResourceDefinition, opts) } // Create mocks base method. func (m *MockCustomResourceDefinitionInterface) Create(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts v11.CreateOptions) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", ctx, customResourceDefinition, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Create(ctx, customResourceDefinition, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Create), ctx, customResourceDefinition, opts) } // Delete mocks base method. func (m *MockCustomResourceDefinitionInterface) Delete(ctx context.Context, name string, opts v11.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", ctx, name, opts) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Delete(ctx, name, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Delete), ctx, name, opts) } // DeleteCollection mocks base method. func (m *MockCustomResourceDefinitionInterface) DeleteCollection(ctx context.Context, opts v11.DeleteOptions, listOpts v11.ListOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteCollection", ctx, opts, listOpts) ret0, _ := ret[0].(error) return ret0 } // DeleteCollection indicates an expected call of DeleteCollection. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) DeleteCollection(ctx, opts, listOpts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).DeleteCollection), ctx, opts, listOpts) } // Get mocks base method. func (m *MockCustomResourceDefinitionInterface) Get(ctx context.Context, name string, opts v11.GetOptions) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, name, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Get(ctx, name, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Get), ctx, name, opts) } // List mocks base method. func (m *MockCustomResourceDefinitionInterface) List(ctx context.Context, opts v11.ListOptions) (*v1.CustomResourceDefinitionList, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", ctx, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinitionList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) List(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).List), ctx, opts) } // Patch mocks base method. func (m *MockCustomResourceDefinitionInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v11.PatchOptions, subresources ...string) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() varargs := []any{ctx, name, pt, data, opts} for _, a := range subresources { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Patch", varargs...) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // Patch indicates an expected call of Patch. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Patch(ctx, name, pt, data, opts any, subresources ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{ctx, name, pt, data, opts}, subresources...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Patch), varargs...) } // Update mocks base method. func (m *MockCustomResourceDefinitionInterface) Update(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts v11.UpdateOptions) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", ctx, customResourceDefinition, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Update(ctx, customResourceDefinition, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Update), ctx, customResourceDefinition, opts) } // UpdateStatus mocks base method. func (m *MockCustomResourceDefinitionInterface) UpdateStatus(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts v11.UpdateOptions) (*v1.CustomResourceDefinition, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStatus", ctx, customResourceDefinition, opts) ret0, _ := ret[0].(*v1.CustomResourceDefinition) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateStatus indicates an expected call of UpdateStatus. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) UpdateStatus(ctx, customResourceDefinition, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).UpdateStatus), ctx, customResourceDefinition, opts) } // Watch mocks base method. func (m *MockCustomResourceDefinitionInterface) Watch(ctx context.Context, opts v11.ListOptions) (watch.Interface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Watch", ctx, opts) ret0, _ := ret[0].(watch.Interface) ret1, _ := ret[1].(error) return ret0, ret1 } // Watch indicates an expected call of Watch. func (mr *MockCustomResourceDefinitionInterfaceMockRecorder) Watch(ctx, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockCustomResourceDefinitionInterface)(nil).Watch), ctx, opts) } ================================================ FILE: pkg/crd/print.go ================================================ package crd import ( "context" "io" "os" "path/filepath" "github.com/rancher/wrangler/v3/pkg/yaml" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" ) func WriteFile(filename string, crds []CRD) error { if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { return err } f, err := os.Create(filename) if err != nil { return err } defer f.Close() return Print(f, crds) } func Print(out io.Writer, crds []CRD) error { obj, err := Objects(crds) if err != nil { return err } data, err := yaml.Export(obj...) if err != nil { return err } _, err = out.Write(data) return err } func Objects(crds []CRD) (result []runtime.Object, err error) { for _, crdDef := range crds { if crdDef.Override == nil { crd, err := crdDef.ToCustomResourceDefinition() if err != nil { return nil, err } result = append(result, crd) } else { result = append(result, crdDef.Override) } } return } func Create(ctx context.Context, cfg *rest.Config, crds []CRD) error { factory, err := NewFactoryFromClient(cfg) if err != nil { return err } return factory.BatchCreateCRDs(ctx, crds...).BatchWait() } ================================================ FILE: pkg/data/convert/convert.go ================================================ package convert import ( "bytes" "encoding/json" "errors" "fmt" "strconv" "strings" "time" "unicode" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func Singular(value interface{}) interface{} { if slice, ok := value.([]string); ok { if len(slice) == 0 { return nil } return slice[0] } if slice, ok := value.([]interface{}); ok { if len(slice) == 0 { return nil } return slice[0] } return value } func ToStringNoTrim(value interface{}) string { if t, ok := value.(time.Time); ok { return t.Format(time.RFC3339) } single := Singular(value) if single == nil { return "" } return fmt.Sprint(single) } func ToString(value interface{}) string { return strings.TrimSpace(ToStringNoTrim(value)) } func ToTimestamp(value interface{}) (int64, error) { str := ToString(value) if str == "" { return 0, errors.New("invalid date") } t, err := time.Parse(time.RFC3339, str) if err != nil { return 0, err } return t.UnixNano() / 1000000, nil } func ToBool(value interface{}) bool { value = Singular(value) b, ok := value.(bool) if ok { return b } str := strings.ToLower(ToString(value)) return str == "true" || str == "t" || str == "yes" || str == "y" } func ToNumber(value interface{}) (int64, error) { value = Singular(value) i, ok := value.(int64) if ok { return i, nil } f, ok := value.(float64) if ok { return int64(f), nil } if n, ok := value.(json.Number); ok { i, err := n.Int64() if err == nil { return i, nil } f, err := n.Float64() return int64(f), err } return strconv.ParseInt(ToString(value), 10, 64) } func ToFloat(value interface{}) (float64, error) { value = Singular(value) f64, ok := value.(float64) if ok { return f64, nil } f32, ok := value.(float32) if ok { return float64(f32), nil } if n, ok := value.(json.Number); ok { i, err := n.Int64() if err == nil { return float64(i), nil } f, err := n.Float64() return float64(f), err } return strconv.ParseFloat(ToString(value), 64) } func Capitalize(s string) string { if len(s) <= 1 { return strings.ToUpper(s) } return strings.ToUpper(s[:1]) + s[1:] } func Uncapitalize(s string) string { if len(s) <= 1 { return strings.ToLower(s) } return strings.ToLower(s[:1]) + s[1:] } func LowerTitle(input string) string { runes := []rune(input) for i := 0; i < len(runes); i++ { if unicode.IsUpper(runes[i]) && (i == 0 || i == len(runes)-1 || unicode.IsUpper(runes[i+1])) { runes[i] = unicode.ToLower(runes[i]) } else { break } } return string(runes) } func IsEmptyValue(v interface{}) bool { if v == nil || v == "" || v == 0 || v == false { return true } if m, ok := v.(map[string]interface{}); ok { return len(m) == 0 } if s, ok := v.([]interface{}); ok { return len(s) == 0 } return false } func ToMapInterface(obj interface{}) map[string]interface{} { v, _ := obj.(map[string]interface{}) return v } func ToInterfaceSlice(obj interface{}) []interface{} { if v, ok := obj.([]interface{}); ok { return v } return nil } func ToMapSlice(obj interface{}) []map[string]interface{} { if v, ok := obj.([]map[string]interface{}); ok { return v } vs, _ := obj.([]interface{}) var result []map[string]interface{} for _, item := range vs { if v, ok := item.(map[string]interface{}); ok { result = append(result, v) } else { return nil } } return result } func ToStringSlice(data interface{}) []string { if v, ok := data.([]string); ok { return v } if v, ok := data.([]interface{}); ok { var result []string for _, item := range v { result = append(result, ToString(item)) } return result } if v, ok := data.(string); ok { return []string{v} } return nil } func ToObj(data interface{}, into interface{}) error { bytes, err := json.Marshal(data) if err != nil { return err } return json.Unmarshal(bytes, into) } func EncodeToMap(obj interface{}) (map[string]interface{}, error) { if m, ok := obj.(map[string]interface{}); ok { return m, nil } if unstr, ok := obj.(*unstructured.Unstructured); ok { return unstr.Object, nil } b, err := json.Marshal(obj) if err != nil { return nil, err } result := map[string]interface{}{} dec := json.NewDecoder(bytes.NewBuffer(b)) dec.UseNumber() return result, dec.Decode(&result) } func ToJSONKey(str string) string { parts := strings.Split(str, "_") for i := 1; i < len(parts); i++ { caser := cases.Title(language.English) parts[i] = caser.String(parts[i]) } return strings.Join(parts, "") } func ToYAMLKey(str string) string { var result []rune cap := false for i, r := range []rune(str) { if i == 0 { if unicode.IsUpper(r) { cap = true } result = append(result, unicode.ToLower(r)) continue } if unicode.IsUpper(r) { if cap { result = append(result, unicode.ToLower(r)) } else { result = append(result, '_', unicode.ToLower(r)) } } else { cap = false result = append(result, r) } } return string(result) } func ToArgKey(str string) string { var ( result []rune input = []rune(str) ) cap := false for i := 0; i < len(input); i++ { r := input[i] if i == 0 { if unicode.IsUpper(r) { cap = true } result = append(result, unicode.ToLower(r)) continue } if unicode.IsUpper(r) { if cap { result = append(result, unicode.ToLower(r)) } else if len(input) > i+2 && unicode.IsUpper(input[i]) && unicode.IsUpper(input[i+1]) && unicode.IsUpper(input[i+2]) { result = append(result, '-', unicode.ToLower(input[i]), unicode.ToLower(input[i+1]), unicode.ToLower(input[i+2])) i += 2 } else { result = append(result, '-', unicode.ToLower(r)) } } else { cap = false result = append(result, r) } } return "--" + string(result) } ================================================ FILE: pkg/data/convert/convert_test.go ================================================ package convert import ( "testing" ) type data struct { TTLMillis int `json:"ttl"` } func TestJSON(t *testing.T) { d := &data{ TTLMillis: 57600000, } m, err := EncodeToMap(d) if err != nil { t.Fatal(err) } i, _ := ToNumber(m["ttl"]) if i != 57600000 { t.Fatal("not", 57600000, "got", m["ttl"]) } } func TestArgKey(t *testing.T) { data := []struct { input string output string }{ { input: "disableOpenAPIValidation", output: "--disable-open-api-validation", }, { input: "skipCRDs", output: "--skip-crds", }, } for _, data := range data { if ToArgKey(data.input) != data.output { t.Errorf("expected %s, got %s", data.output, ToArgKey(data.input)) } } } ================================================ FILE: pkg/data/data.go ================================================ package data import ( "github.com/rancher/wrangler/v3/pkg/data/convert" ) type List []map[string]interface{} type Object map[string]interface{} func New() Object { return map[string]interface{}{} } func Convert(obj interface{}) (Object, error) { data, err := convert.EncodeToMap(obj) if err != nil { return nil, err } return data, nil } func (o Object) Map(names ...string) Object { v := GetValueN(o, names...) m := convert.ToMapInterface(v) return m } func (o Object) Slice(names ...string) (result []Object) { v := GetValueN(o, names...) for _, item := range convert.ToInterfaceSlice(v) { result = append(result, convert.ToMapInterface(item)) } return } func (o Object) Values() (result []Object) { for k := range o { result = append(result, o.Map(k)) } return } func (o Object) String(names ...string) string { v := GetValueN(o, names...) return convert.ToString(v) } func (o Object) StringSlice(names ...string) []string { v := GetValueN(o, names...) return convert.ToStringSlice(v) } func (o Object) Set(key string, obj interface{}) { if o == nil { return } o[key] = obj } func (o Object) SetNested(obj interface{}, key ...string) { PutValue(o, obj, key...) } func (o Object) Bool(key ...string) bool { return convert.ToBool(GetValueN(o, key...)) } ================================================ FILE: pkg/data/merge.go ================================================ package data func MergeMaps(base, overlay map[string]interface{}) map[string]interface{} { result := map[string]interface{}{} for k, v := range base { result[k] = v } for k, v := range overlay { if baseMap, overlayMap, bothMaps := bothMaps(result[k], v); bothMaps { v = MergeMaps(baseMap, overlayMap) } result[k] = v } return result } func bothMaps(left, right interface{}) (map[string]interface{}, map[string]interface{}, bool) { leftMap, ok := left.(map[string]interface{}) if !ok { return nil, nil, false } rightMap, ok := right.(map[string]interface{}) return leftMap, rightMap, ok } func bothSlices(left, right interface{}) ([]interface{}, []interface{}, bool) { leftSlice, ok := left.([]interface{}) if !ok { return nil, nil, false } rightSlice, ok := right.([]interface{}) return leftSlice, rightSlice, ok } func MergeMapsConcatSlice(base, overlay map[string]interface{}) map[string]interface{} { result := map[string]interface{}{} for k, v := range base { result[k] = v } for k, v := range overlay { if baseMap, overlayMap, bothMaps := bothMaps(result[k], v); bothMaps { v = MergeMaps(baseMap, overlayMap) } else if baseSlice, overlaySlice, bothSlices := bothSlices(result[k], v); bothSlices { s := make([]interface{}, 0, len(baseSlice)+len(overlaySlice)) s = append(s, baseSlice...) s = append(s, overlaySlice...) v = s } result[k] = v } return result } ================================================ FILE: pkg/data/values.go ================================================ // Package data contains functions for working with unstructured values like []interface or map[string]interface{}. // It allows reading/writing to these values without having to convert to structured items. package data import ( "strconv" ) // RemoveValue removes a value from data. Keys should be in order denoting the path to the value in the nested // structure of the map. For example, passing []string{"metadata", "annotations"} will make the function remove the // "annotations" key from the "metadata" sub-map. Returns the removed value (if any) and a bool indicating if the value // was found. func RemoveValue(data map[string]interface{}, keys ...string) (interface{}, bool) { for i, key := range keys { if i == len(keys)-1 { val, ok := data[key] delete(data, key) return val, ok } data, _ = data[key].(map[string]interface{}) } return nil, false } func GetValueN(data map[string]interface{}, keys ...string) interface{} { val, _ := GetValue(data, keys...) return val } // GetValue works similar to GetValueFromAny, but can only process maps. Kept this way to avoid breaking changes with // the previous interface, GetValueFromAny should be used in most cases since that can handle slices as well. func GetValue(data map[string]interface{}, keys ...string) (interface{}, bool) { for i, key := range keys { if i == len(keys)-1 { val, ok := data[key] return val, ok } data, _ = data[key].(map[string]interface{}) } return nil, false } // GetValueFromAny retrieves a value from the provided collection, which must be a map[string]interface, []interface, or []string (as a final value) // Keys are always strings. // For a map, a key denotes the key in the map whose value we want to retrieve. // For the slice, it denotes the index (starting at 0) of the value we want to retrieve. // Returns the retrieved value (if any) and a bool indicating if the value was found. func GetValueFromAny(data interface{}, keys ...string) (interface{}, bool) { if len(keys) == 0 { return nil, false } for _, key := range keys { if d2, ok := data.(map[string]interface{}); ok { data, ok = d2[key] if !ok { return nil, false } continue } // So it must be an array. Verify the index and then continue // with a type-assertion switch block for the two different types of arrays we expect keyInt, err := strconv.Atoi(key) if err != nil || keyInt < 0 { return nil, false } switch node := data.(type) { case []interface{}: if keyInt >= len(node) { return nil, false } data = node[keyInt] case []string: if keyInt >= len(node) { return nil, false } data = node[keyInt] // If we're at the end of the keys, we'll return the value at the end of this function // Otherwise we'll try to index into the string and hit the default case, // and return // See the "keys nested too far on a string array" test. default: return nil, false } } return data, true } // PutValue updates the value of a given map at the index specified by keys that denote the path to the value in the // nested structure of the map. If there is no current entry at a key, a new map is created for that value. func PutValue(data map[string]interface{}, val interface{}, keys ...string) { if data == nil { return } // This is so ugly for i, key := range keys { if i == len(keys)-1 { data[key] = val } else { newData, ok := data[key] if ok { newMap, ok := newData.(map[string]interface{}) if ok { data = newMap } else { return } } else { newMap := map[string]interface{}{} data[key] = newMap data = newMap } } } } ================================================ FILE: pkg/data/values_test.go ================================================ package data import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetValueFromAny(t *testing.T) { t.Parallel() tests := []struct { name string data interface{} keys []string wantValue interface{} wantSuccess bool }{ { name: "nil map", data: nil, keys: []string{"somekey"}, wantValue: nil, wantSuccess: false, }, { name: "key is not in map", data: map[string]interface{}{ "realKey": "realVal", }, keys: []string{"badKey"}, wantValue: nil, wantSuccess: false, }, { name: "key is in first level of map", data: map[string]interface{}{ "realKey": "realVal", }, keys: []string{"realKey"}, wantValue: "realVal", wantSuccess: true, }, { name: "key is nested in map", data: map[string]interface{}{ "parent": map[string]interface{}{ "child": map[string]interface{}{ "grandchild": "someValue", }, }, }, keys: []string{"parent", "child", "grandchild"}, wantValue: "someValue", wantSuccess: true, }, { name: "incorrected nested key", data: map[string]interface{}{ "parent": map[string]interface{}{ "child": map[string]interface{}{ "grandchild": "someValue", }, }, }, keys: []string{"parent", "grandchild", "child"}, wantValue: nil, wantSuccess: false, }, { name: "get index of slice", data: map[string]interface{}{ "parent": map[string]interface{}{ "children": []interface{}{ "alice", "bob", "eve", }, }, }, keys: []string{"parent", "children", "2"}, wantValue: "eve", wantSuccess: true, }, { name: "get index of top level slice", data: []interface{}{ "alice", "bob", "eve", }, keys: []string{"2"}, wantValue: "eve", wantSuccess: true, }, { name: "slice of maps", data: []interface{}{ map[string]interface{}{ "notthisone": "val", }, map[string]interface{}{ "parent": map[string]interface{}{ "children": []interface{}{ "alice", "bob", "eve", }, }, }, }, keys: []string{"1", "parent", "children", "0"}, wantValue: "alice", wantSuccess: true, }, { name: "index is too big", data: map[string]interface{}{ "parent": map[string]interface{}{ "children": []interface{}{ "alice", "bob", "eve", }, }, }, keys: []string{"parent", "children", "3"}, wantValue: nil, wantSuccess: false, }, { name: "index is negative", data: map[string]interface{}{ "parent": map[string]interface{}{ "children": []interface{}{ "alice", "bob", "eve", }, }, }, keys: []string{"parent", "children", "-3"}, wantValue: nil, wantSuccess: false, }, { name: "index not parseable to int", data: map[string]interface{}{ "parent": map[string]interface{}{ "children": []interface{}{ "alice", "bob", "eve", }, }, }, keys: []string{"parent", "children", "notanint"}, wantValue: nil, wantSuccess: false, }, { name: "slice blank index", data: []interface{}{ "bob", }, keys: []string{""}, wantValue: nil, wantSuccess: false, }, { name: "slice no index", data: []interface{}{ "bob", }, wantValue: nil, wantSuccess: false, }, { name: "keys nested too far", data: []interface{}{ "alice", "bob", "eve", }, keys: []string{"2", "1"}, wantValue: nil, wantSuccess: false, }, { name: "keys nested too far on a string array", data: map[string]interface{}{ "block1": []string{ "ink", "wink", "blink", }, "block2": []string{ "ball", "bell", "bill", }, }, keys: []string{"block1", "2", "3"}, wantValue: nil, wantSuccess: false, }, { name: "map blank key with value", data: map[string]interface{}{ "": "bob", }, keys: []string{""}, wantValue: "bob", wantSuccess: true, }, { name: "map blank key no value", data: map[string]interface{}{ "alice": "bob", }, keys: []string{""}, wantValue: nil, wantSuccess: false, }, { name: "map no key", data: map[string]interface{}{ "": "bob", }, wantValue: nil, wantSuccess: false, }, { name: "contains an array of strings at top-level", data: map[string]interface{}{ "kind": "apple", "metadata": map[string]interface{}{ "name": "granny-smith", "fields": []string{ "a3", "position2", "more...", }, }, "data": map[string]interface{}{ "color": "green", }, }, keys: []string{"metadata", "fields", "1"}, wantValue: "position2", wantSuccess: true, }, { name: "contains an array of strings at top-level", data: []string{ "a4", "position4", "more...", }, keys: []string{"2"}, wantValue: "more...", wantSuccess: true, }, { name: "index out of bounds for top-level array", data: []string{ "a4", "position4", "more...", }, keys: []string{"-5"}, wantValue: nil, wantSuccess: false, }, { name: "doesn't handle array of ints", data: []int{1, 3, 5}, keys: []string{"1"}, wantValue: nil, wantSuccess: false, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() gotValue, gotSuccess := GetValueFromAny(test.data, test.keys...) assert.Equal(t, test.wantValue, gotValue) assert.Equal(t, test.wantSuccess, gotSuccess) }) } } ================================================ FILE: pkg/generated/controllers/admissionregistration.k8s.io/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package admissionregistration import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Admissionregistration() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/admissionregistration.k8s.io/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package admissionregistration import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/admissionregistration.k8s.io/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/admissionregistration.k8s.io/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { MutatingWebhookConfiguration() MutatingWebhookConfigurationController ValidatingWebhookConfiguration() ValidatingWebhookConfigurationController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) MutatingWebhookConfiguration() MutatingWebhookConfigurationController { return generic.NewNonNamespacedController[*v1.MutatingWebhookConfiguration, *v1.MutatingWebhookConfigurationList](schema.GroupVersionKind{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfiguration"}, "mutatingwebhookconfigurations", v.controllerFactory) } func (v *version) ValidatingWebhookConfiguration() ValidatingWebhookConfigurationController { return generic.NewNonNamespacedController[*v1.ValidatingWebhookConfiguration, *v1.ValidatingWebhookConfigurationList](schema.GroupVersionKind{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfiguration"}, "validatingwebhookconfigurations", v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/admissionregistration.k8s.io/v1/mutatingwebhookconfiguration.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/admissionregistration/v1" ) // MutatingWebhookConfigurationController interface for managing MutatingWebhookConfiguration resources. type MutatingWebhookConfigurationController interface { generic.NonNamespacedControllerInterface[*v1.MutatingWebhookConfiguration, *v1.MutatingWebhookConfigurationList] } // MutatingWebhookConfigurationClient interface for managing MutatingWebhookConfiguration resources in Kubernetes. type MutatingWebhookConfigurationClient interface { generic.NonNamespacedClientInterface[*v1.MutatingWebhookConfiguration, *v1.MutatingWebhookConfigurationList] } // MutatingWebhookConfigurationCache interface for retrieving MutatingWebhookConfiguration resources in memory. type MutatingWebhookConfigurationCache interface { generic.NonNamespacedCacheInterface[*v1.MutatingWebhookConfiguration] } ================================================ FILE: pkg/generated/controllers/admissionregistration.k8s.io/v1/validatingwebhookconfiguration.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/admissionregistration/v1" ) // ValidatingWebhookConfigurationController interface for managing ValidatingWebhookConfiguration resources. type ValidatingWebhookConfigurationController interface { generic.NonNamespacedControllerInterface[*v1.ValidatingWebhookConfiguration, *v1.ValidatingWebhookConfigurationList] } // ValidatingWebhookConfigurationClient interface for managing ValidatingWebhookConfiguration resources in Kubernetes. type ValidatingWebhookConfigurationClient interface { generic.NonNamespacedClientInterface[*v1.ValidatingWebhookConfiguration, *v1.ValidatingWebhookConfigurationList] } // ValidatingWebhookConfigurationCache interface for retrieving ValidatingWebhookConfiguration resources in memory. type ValidatingWebhookConfigurationCache interface { generic.NonNamespacedCacheInterface[*v1.ValidatingWebhookConfiguration] } ================================================ FILE: pkg/generated/controllers/apiextensions.k8s.io/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package apiextensions import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Apiextensions() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/apiextensions.k8s.io/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package apiextensions import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/apiextensions.k8s.io/v1/customresourcedefinition.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // CustomResourceDefinitionController interface for managing CustomResourceDefinition resources. type CustomResourceDefinitionController interface { generic.NonNamespacedControllerInterface[*v1.CustomResourceDefinition, *v1.CustomResourceDefinitionList] } // CustomResourceDefinitionClient interface for managing CustomResourceDefinition resources in Kubernetes. type CustomResourceDefinitionClient interface { generic.NonNamespacedClientInterface[*v1.CustomResourceDefinition, *v1.CustomResourceDefinitionList] } // CustomResourceDefinitionCache interface for retrieving CustomResourceDefinition resources in memory. type CustomResourceDefinitionCache interface { generic.NonNamespacedCacheInterface[*v1.CustomResourceDefinition] } // CustomResourceDefinitionStatusHandler is executed for every added or modified CustomResourceDefinition. Should return the new status to be updated type CustomResourceDefinitionStatusHandler func(obj *v1.CustomResourceDefinition, status v1.CustomResourceDefinitionStatus) (v1.CustomResourceDefinitionStatus, error) // CustomResourceDefinitionGeneratingHandler is the top-level handler that is executed for every CustomResourceDefinition event. It extends CustomResourceDefinitionStatusHandler by a returning a slice of child objects to be passed to apply.Apply type CustomResourceDefinitionGeneratingHandler func(obj *v1.CustomResourceDefinition, status v1.CustomResourceDefinitionStatus) ([]runtime.Object, v1.CustomResourceDefinitionStatus, error) // RegisterCustomResourceDefinitionStatusHandler configures a CustomResourceDefinitionController to execute a CustomResourceDefinitionStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterCustomResourceDefinitionStatusHandler(ctx context.Context, controller CustomResourceDefinitionController, condition condition.Cond, name string, handler CustomResourceDefinitionStatusHandler) { statusHandler := &customResourceDefinitionStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterCustomResourceDefinitionGeneratingHandler configures a CustomResourceDefinitionController to execute a CustomResourceDefinitionGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterCustomResourceDefinitionGeneratingHandler(ctx context.Context, controller CustomResourceDefinitionController, apply apply.Apply, condition condition.Cond, name string, handler CustomResourceDefinitionGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &customResourceDefinitionGeneratingHandler{ CustomResourceDefinitionGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterCustomResourceDefinitionStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type customResourceDefinitionStatusHandler struct { client CustomResourceDefinitionClient condition condition.Cond handler CustomResourceDefinitionStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *customResourceDefinitionStatusHandler) sync(key string, obj *v1.CustomResourceDefinition) (*v1.CustomResourceDefinition, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type customResourceDefinitionGeneratingHandler struct { CustomResourceDefinitionGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *customResourceDefinitionGeneratingHandler) Remove(key string, obj *v1.CustomResourceDefinition) (*v1.CustomResourceDefinition, error) { if obj != nil { return obj, nil } obj = &v1.CustomResourceDefinition{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured CustomResourceDefinitionGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *customResourceDefinitionGeneratingHandler) Handle(obj *v1.CustomResourceDefinition, status v1.CustomResourceDefinitionStatus) (v1.CustomResourceDefinitionStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.CustomResourceDefinitionGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *customResourceDefinitionGeneratingHandler) isNewResourceVersion(obj *v1.CustomResourceDefinition) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *customResourceDefinitionGeneratingHandler) storeResourceVersion(obj *v1.CustomResourceDefinition) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/apiextensions.k8s.io/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { CustomResourceDefinition() CustomResourceDefinitionController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) CustomResourceDefinition() CustomResourceDefinitionController { return generic.NewNonNamespacedController[*v1.CustomResourceDefinition, *v1.CustomResourceDefinitionList](schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}, "customresourcedefinitions", v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/apiregistration.k8s.io/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package apiregistration import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Apiregistration() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/apiregistration.k8s.io/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package apiregistration import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/apiregistration.k8s.io/v1/apiservice.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" ) // APIServiceController interface for managing APIService resources. type APIServiceController interface { generic.NonNamespacedControllerInterface[*v1.APIService, *v1.APIServiceList] } // APIServiceClient interface for managing APIService resources in Kubernetes. type APIServiceClient interface { generic.NonNamespacedClientInterface[*v1.APIService, *v1.APIServiceList] } // APIServiceCache interface for retrieving APIService resources in memory. type APIServiceCache interface { generic.NonNamespacedCacheInterface[*v1.APIService] } // APIServiceStatusHandler is executed for every added or modified APIService. Should return the new status to be updated type APIServiceStatusHandler func(obj *v1.APIService, status v1.APIServiceStatus) (v1.APIServiceStatus, error) // APIServiceGeneratingHandler is the top-level handler that is executed for every APIService event. It extends APIServiceStatusHandler by a returning a slice of child objects to be passed to apply.Apply type APIServiceGeneratingHandler func(obj *v1.APIService, status v1.APIServiceStatus) ([]runtime.Object, v1.APIServiceStatus, error) // RegisterAPIServiceStatusHandler configures a APIServiceController to execute a APIServiceStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterAPIServiceStatusHandler(ctx context.Context, controller APIServiceController, condition condition.Cond, name string, handler APIServiceStatusHandler) { statusHandler := &aPIServiceStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterAPIServiceGeneratingHandler configures a APIServiceController to execute a APIServiceGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterAPIServiceGeneratingHandler(ctx context.Context, controller APIServiceController, apply apply.Apply, condition condition.Cond, name string, handler APIServiceGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &aPIServiceGeneratingHandler{ APIServiceGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterAPIServiceStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type aPIServiceStatusHandler struct { client APIServiceClient condition condition.Cond handler APIServiceStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *aPIServiceStatusHandler) sync(key string, obj *v1.APIService) (*v1.APIService, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type aPIServiceGeneratingHandler struct { APIServiceGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *aPIServiceGeneratingHandler) Remove(key string, obj *v1.APIService) (*v1.APIService, error) { if obj != nil { return obj, nil } obj = &v1.APIService{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured APIServiceGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *aPIServiceGeneratingHandler) Handle(obj *v1.APIService, status v1.APIServiceStatus) (v1.APIServiceStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.APIServiceGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *aPIServiceGeneratingHandler) isNewResourceVersion(obj *v1.APIService) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *aPIServiceGeneratingHandler) storeResourceVersion(obj *v1.APIService) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/apiregistration.k8s.io/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" "k8s.io/apimachinery/pkg/runtime/schema" v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { APIService() APIServiceController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) APIService() APIServiceController { return generic.NewNonNamespacedController[*v1.APIService, *v1.APIServiceList](schema.GroupVersionKind{Group: "apiregistration.k8s.io", Version: "v1", Kind: "APIService"}, "apiservices", v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/apps/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package apps import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Apps() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/apps/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package apps import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apps/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/apps/v1/daemonset.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // DaemonSetController interface for managing DaemonSet resources. type DaemonSetController interface { generic.ControllerInterface[*v1.DaemonSet, *v1.DaemonSetList] } // DaemonSetClient interface for managing DaemonSet resources in Kubernetes. type DaemonSetClient interface { generic.ClientInterface[*v1.DaemonSet, *v1.DaemonSetList] } // DaemonSetCache interface for retrieving DaemonSet resources in memory. type DaemonSetCache interface { generic.CacheInterface[*v1.DaemonSet] } // DaemonSetStatusHandler is executed for every added or modified DaemonSet. Should return the new status to be updated type DaemonSetStatusHandler func(obj *v1.DaemonSet, status v1.DaemonSetStatus) (v1.DaemonSetStatus, error) // DaemonSetGeneratingHandler is the top-level handler that is executed for every DaemonSet event. It extends DaemonSetStatusHandler by a returning a slice of child objects to be passed to apply.Apply type DaemonSetGeneratingHandler func(obj *v1.DaemonSet, status v1.DaemonSetStatus) ([]runtime.Object, v1.DaemonSetStatus, error) // RegisterDaemonSetStatusHandler configures a DaemonSetController to execute a DaemonSetStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterDaemonSetStatusHandler(ctx context.Context, controller DaemonSetController, condition condition.Cond, name string, handler DaemonSetStatusHandler) { statusHandler := &daemonSetStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterDaemonSetGeneratingHandler configures a DaemonSetController to execute a DaemonSetGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterDaemonSetGeneratingHandler(ctx context.Context, controller DaemonSetController, apply apply.Apply, condition condition.Cond, name string, handler DaemonSetGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &daemonSetGeneratingHandler{ DaemonSetGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterDaemonSetStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type daemonSetStatusHandler struct { client DaemonSetClient condition condition.Cond handler DaemonSetStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *daemonSetStatusHandler) sync(key string, obj *v1.DaemonSet) (*v1.DaemonSet, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type daemonSetGeneratingHandler struct { DaemonSetGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *daemonSetGeneratingHandler) Remove(key string, obj *v1.DaemonSet) (*v1.DaemonSet, error) { if obj != nil { return obj, nil } obj = &v1.DaemonSet{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured DaemonSetGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *daemonSetGeneratingHandler) Handle(obj *v1.DaemonSet, status v1.DaemonSetStatus) (v1.DaemonSetStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.DaemonSetGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *daemonSetGeneratingHandler) isNewResourceVersion(obj *v1.DaemonSet) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *daemonSetGeneratingHandler) storeResourceVersion(obj *v1.DaemonSet) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/apps/v1/deployment.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // DeploymentController interface for managing Deployment resources. type DeploymentController interface { generic.ControllerInterface[*v1.Deployment, *v1.DeploymentList] } // DeploymentClient interface for managing Deployment resources in Kubernetes. type DeploymentClient interface { generic.ClientInterface[*v1.Deployment, *v1.DeploymentList] } // DeploymentCache interface for retrieving Deployment resources in memory. type DeploymentCache interface { generic.CacheInterface[*v1.Deployment] } // DeploymentStatusHandler is executed for every added or modified Deployment. Should return the new status to be updated type DeploymentStatusHandler func(obj *v1.Deployment, status v1.DeploymentStatus) (v1.DeploymentStatus, error) // DeploymentGeneratingHandler is the top-level handler that is executed for every Deployment event. It extends DeploymentStatusHandler by a returning a slice of child objects to be passed to apply.Apply type DeploymentGeneratingHandler func(obj *v1.Deployment, status v1.DeploymentStatus) ([]runtime.Object, v1.DeploymentStatus, error) // RegisterDeploymentStatusHandler configures a DeploymentController to execute a DeploymentStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterDeploymentStatusHandler(ctx context.Context, controller DeploymentController, condition condition.Cond, name string, handler DeploymentStatusHandler) { statusHandler := &deploymentStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterDeploymentGeneratingHandler configures a DeploymentController to execute a DeploymentGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterDeploymentGeneratingHandler(ctx context.Context, controller DeploymentController, apply apply.Apply, condition condition.Cond, name string, handler DeploymentGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &deploymentGeneratingHandler{ DeploymentGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterDeploymentStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type deploymentStatusHandler struct { client DeploymentClient condition condition.Cond handler DeploymentStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *deploymentStatusHandler) sync(key string, obj *v1.Deployment) (*v1.Deployment, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type deploymentGeneratingHandler struct { DeploymentGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *deploymentGeneratingHandler) Remove(key string, obj *v1.Deployment) (*v1.Deployment, error) { if obj != nil { return obj, nil } obj = &v1.Deployment{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured DeploymentGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *deploymentGeneratingHandler) Handle(obj *v1.Deployment, status v1.DeploymentStatus) (v1.DeploymentStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.DeploymentGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *deploymentGeneratingHandler) isNewResourceVersion(obj *v1.Deployment) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *deploymentGeneratingHandler) storeResourceVersion(obj *v1.Deployment) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/apps/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { DaemonSet() DaemonSetController Deployment() DeploymentController StatefulSet() StatefulSetController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) DaemonSet() DaemonSetController { return generic.NewController[*v1.DaemonSet, *v1.DaemonSetList](schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "DaemonSet"}, "daemonsets", true, v.controllerFactory) } func (v *version) Deployment() DeploymentController { return generic.NewController[*v1.Deployment, *v1.DeploymentList](schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "deployments", true, v.controllerFactory) } func (v *version) StatefulSet() StatefulSetController { return generic.NewController[*v1.StatefulSet, *v1.StatefulSetList](schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}, "statefulsets", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/apps/v1/statefulset.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // StatefulSetController interface for managing StatefulSet resources. type StatefulSetController interface { generic.ControllerInterface[*v1.StatefulSet, *v1.StatefulSetList] } // StatefulSetClient interface for managing StatefulSet resources in Kubernetes. type StatefulSetClient interface { generic.ClientInterface[*v1.StatefulSet, *v1.StatefulSetList] } // StatefulSetCache interface for retrieving StatefulSet resources in memory. type StatefulSetCache interface { generic.CacheInterface[*v1.StatefulSet] } // StatefulSetStatusHandler is executed for every added or modified StatefulSet. Should return the new status to be updated type StatefulSetStatusHandler func(obj *v1.StatefulSet, status v1.StatefulSetStatus) (v1.StatefulSetStatus, error) // StatefulSetGeneratingHandler is the top-level handler that is executed for every StatefulSet event. It extends StatefulSetStatusHandler by a returning a slice of child objects to be passed to apply.Apply type StatefulSetGeneratingHandler func(obj *v1.StatefulSet, status v1.StatefulSetStatus) ([]runtime.Object, v1.StatefulSetStatus, error) // RegisterStatefulSetStatusHandler configures a StatefulSetController to execute a StatefulSetStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterStatefulSetStatusHandler(ctx context.Context, controller StatefulSetController, condition condition.Cond, name string, handler StatefulSetStatusHandler) { statusHandler := &statefulSetStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterStatefulSetGeneratingHandler configures a StatefulSetController to execute a StatefulSetGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterStatefulSetGeneratingHandler(ctx context.Context, controller StatefulSetController, apply apply.Apply, condition condition.Cond, name string, handler StatefulSetGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &statefulSetGeneratingHandler{ StatefulSetGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterStatefulSetStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type statefulSetStatusHandler struct { client StatefulSetClient condition condition.Cond handler StatefulSetStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *statefulSetStatusHandler) sync(key string, obj *v1.StatefulSet) (*v1.StatefulSet, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type statefulSetGeneratingHandler struct { StatefulSetGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *statefulSetGeneratingHandler) Remove(key string, obj *v1.StatefulSet) (*v1.StatefulSet, error) { if obj != nil { return obj, nil } obj = &v1.StatefulSet{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured StatefulSetGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *statefulSetGeneratingHandler) Handle(obj *v1.StatefulSet, status v1.StatefulSetStatus) (v1.StatefulSetStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.StatefulSetGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *statefulSetGeneratingHandler) isNewResourceVersion(obj *v1.StatefulSet) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *statefulSetGeneratingHandler) storeResourceVersion(obj *v1.StatefulSet) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/batch/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package batch import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Batch() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/batch/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package batch import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/batch/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/batch/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { Job() JobController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) Job() JobController { return generic.NewController[*v1.Job, *v1.JobList](schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}, "jobs", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/batch/v1/job.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // JobController interface for managing Job resources. type JobController interface { generic.ControllerInterface[*v1.Job, *v1.JobList] } // JobClient interface for managing Job resources in Kubernetes. type JobClient interface { generic.ClientInterface[*v1.Job, *v1.JobList] } // JobCache interface for retrieving Job resources in memory. type JobCache interface { generic.CacheInterface[*v1.Job] } // JobStatusHandler is executed for every added or modified Job. Should return the new status to be updated type JobStatusHandler func(obj *v1.Job, status v1.JobStatus) (v1.JobStatus, error) // JobGeneratingHandler is the top-level handler that is executed for every Job event. It extends JobStatusHandler by a returning a slice of child objects to be passed to apply.Apply type JobGeneratingHandler func(obj *v1.Job, status v1.JobStatus) ([]runtime.Object, v1.JobStatus, error) // RegisterJobStatusHandler configures a JobController to execute a JobStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterJobStatusHandler(ctx context.Context, controller JobController, condition condition.Cond, name string, handler JobStatusHandler) { statusHandler := &jobStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterJobGeneratingHandler configures a JobController to execute a JobGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterJobGeneratingHandler(ctx context.Context, controller JobController, apply apply.Apply, condition condition.Cond, name string, handler JobGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &jobGeneratingHandler{ JobGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterJobStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type jobStatusHandler struct { client JobClient condition condition.Cond handler JobStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *jobStatusHandler) sync(key string, obj *v1.Job) (*v1.Job, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type jobGeneratingHandler struct { JobGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *jobGeneratingHandler) Remove(key string, obj *v1.Job) (*v1.Job, error) { if obj != nil { return obj, nil } obj = &v1.Job{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured JobGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *jobGeneratingHandler) Handle(obj *v1.Job, status v1.JobStatus) (v1.JobStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.JobGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *jobGeneratingHandler) isNewResourceVersion(obj *v1.Job) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *jobGeneratingHandler) storeResourceVersion(obj *v1.Job) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/coordination.k8s.io/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package coordination import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Coordination() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/coordination.k8s.io/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package coordination import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/coordination.k8s.io/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/coordination.k8s.io/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/coordination/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { Lease() LeaseController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) Lease() LeaseController { return generic.NewController[*v1.Lease, *v1.LeaseList](schema.GroupVersionKind{Group: "coordination.k8s.io", Version: "v1", Kind: "Lease"}, "leases", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/coordination.k8s.io/v1/lease.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/coordination/v1" ) // LeaseController interface for managing Lease resources. type LeaseController interface { generic.ControllerInterface[*v1.Lease, *v1.LeaseList] } // LeaseClient interface for managing Lease resources in Kubernetes. type LeaseClient interface { generic.ClientInterface[*v1.Lease, *v1.LeaseList] } // LeaseCache interface for retrieving Lease resources in memory. type LeaseCache interface { generic.CacheInterface[*v1.Lease] } ================================================ FILE: pkg/generated/controllers/core/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package core import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Core() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/core/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package core import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/core/v1/configmap.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/core/v1" ) // ConfigMapController interface for managing ConfigMap resources. type ConfigMapController interface { generic.ControllerInterface[*v1.ConfigMap, *v1.ConfigMapList] } // ConfigMapClient interface for managing ConfigMap resources in Kubernetes. type ConfigMapClient interface { generic.ClientInterface[*v1.ConfigMap, *v1.ConfigMapList] } // ConfigMapCache interface for retrieving ConfigMap resources in memory. type ConfigMapCache interface { generic.CacheInterface[*v1.ConfigMap] } ================================================ FILE: pkg/generated/controllers/core/v1/endpoints.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/core/v1" ) // EndpointsController interface for managing Endpoints resources. type EndpointsController interface { generic.ControllerInterface[*v1.Endpoints, *v1.EndpointsList] } // EndpointsClient interface for managing Endpoints resources in Kubernetes. type EndpointsClient interface { generic.ClientInterface[*v1.Endpoints, *v1.EndpointsList] } // EndpointsCache interface for retrieving Endpoints resources in memory. type EndpointsCache interface { generic.CacheInterface[*v1.Endpoints] } ================================================ FILE: pkg/generated/controllers/core/v1/event.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/core/v1" ) // EventController interface for managing Event resources. type EventController interface { generic.ControllerInterface[*v1.Event, *v1.EventList] } // EventClient interface for managing Event resources in Kubernetes. type EventClient interface { generic.ClientInterface[*v1.Event, *v1.EventList] } // EventCache interface for retrieving Event resources in memory. type EventCache interface { generic.CacheInterface[*v1.Event] } ================================================ FILE: pkg/generated/controllers/core/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { ConfigMap() ConfigMapController Endpoints() EndpointsController Event() EventController LimitRange() LimitRangeController Namespace() NamespaceController Node() NodeController PersistentVolume() PersistentVolumeController PersistentVolumeClaim() PersistentVolumeClaimController Pod() PodController ResourceQuota() ResourceQuotaController Secret() SecretController Service() ServiceController ServiceAccount() ServiceAccountController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) ConfigMap() ConfigMapController { return generic.NewController[*v1.ConfigMap, *v1.ConfigMapList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, "configmaps", true, v.controllerFactory) } func (v *version) Endpoints() EndpointsController { return generic.NewController[*v1.Endpoints, *v1.EndpointsList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"}, "endpoints", true, v.controllerFactory) } func (v *version) Event() EventController { return generic.NewController[*v1.Event, *v1.EventList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Event"}, "events", true, v.controllerFactory) } func (v *version) LimitRange() LimitRangeController { return generic.NewController[*v1.LimitRange, *v1.LimitRangeList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "LimitRange"}, "limitranges", true, v.controllerFactory) } func (v *version) Namespace() NamespaceController { return generic.NewNonNamespacedController[*v1.Namespace, *v1.NamespaceList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, "namespaces", v.controllerFactory) } func (v *version) Node() NodeController { return generic.NewNonNamespacedController[*v1.Node, *v1.NodeList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}, "nodes", v.controllerFactory) } func (v *version) PersistentVolume() PersistentVolumeController { return generic.NewNonNamespacedController[*v1.PersistentVolume, *v1.PersistentVolumeList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolume"}, "persistentvolumes", v.controllerFactory) } func (v *version) PersistentVolumeClaim() PersistentVolumeClaimController { return generic.NewController[*v1.PersistentVolumeClaim, *v1.PersistentVolumeClaimList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}, "persistentvolumeclaims", true, v.controllerFactory) } func (v *version) Pod() PodController { return generic.NewController[*v1.Pod, *v1.PodList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, "pods", true, v.controllerFactory) } func (v *version) ResourceQuota() ResourceQuotaController { return generic.NewController[*v1.ResourceQuota, *v1.ResourceQuotaList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ResourceQuota"}, "resourcequotas", true, v.controllerFactory) } func (v *version) Secret() SecretController { return generic.NewController[*v1.Secret, *v1.SecretList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, "secrets", true, v.controllerFactory) } func (v *version) Service() ServiceController { return generic.NewController[*v1.Service, *v1.ServiceList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, "services", true, v.controllerFactory) } func (v *version) ServiceAccount() ServiceAccountController { return generic.NewController[*v1.ServiceAccount, *v1.ServiceAccountList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ServiceAccount"}, "serviceaccounts", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/core/v1/limitrange.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/core/v1" ) // LimitRangeController interface for managing LimitRange resources. type LimitRangeController interface { generic.ControllerInterface[*v1.LimitRange, *v1.LimitRangeList] } // LimitRangeClient interface for managing LimitRange resources in Kubernetes. type LimitRangeClient interface { generic.ClientInterface[*v1.LimitRange, *v1.LimitRangeList] } // LimitRangeCache interface for retrieving LimitRange resources in memory. type LimitRangeCache interface { generic.CacheInterface[*v1.LimitRange] } ================================================ FILE: pkg/generated/controllers/core/v1/namespace.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // NamespaceController interface for managing Namespace resources. type NamespaceController interface { generic.NonNamespacedControllerInterface[*v1.Namespace, *v1.NamespaceList] } // NamespaceClient interface for managing Namespace resources in Kubernetes. type NamespaceClient interface { generic.NonNamespacedClientInterface[*v1.Namespace, *v1.NamespaceList] } // NamespaceCache interface for retrieving Namespace resources in memory. type NamespaceCache interface { generic.NonNamespacedCacheInterface[*v1.Namespace] } // NamespaceStatusHandler is executed for every added or modified Namespace. Should return the new status to be updated type NamespaceStatusHandler func(obj *v1.Namespace, status v1.NamespaceStatus) (v1.NamespaceStatus, error) // NamespaceGeneratingHandler is the top-level handler that is executed for every Namespace event. It extends NamespaceStatusHandler by a returning a slice of child objects to be passed to apply.Apply type NamespaceGeneratingHandler func(obj *v1.Namespace, status v1.NamespaceStatus) ([]runtime.Object, v1.NamespaceStatus, error) // RegisterNamespaceStatusHandler configures a NamespaceController to execute a NamespaceStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterNamespaceStatusHandler(ctx context.Context, controller NamespaceController, condition condition.Cond, name string, handler NamespaceStatusHandler) { statusHandler := &namespaceStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterNamespaceGeneratingHandler configures a NamespaceController to execute a NamespaceGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterNamespaceGeneratingHandler(ctx context.Context, controller NamespaceController, apply apply.Apply, condition condition.Cond, name string, handler NamespaceGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &namespaceGeneratingHandler{ NamespaceGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterNamespaceStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type namespaceStatusHandler struct { client NamespaceClient condition condition.Cond handler NamespaceStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *namespaceStatusHandler) sync(key string, obj *v1.Namespace) (*v1.Namespace, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type namespaceGeneratingHandler struct { NamespaceGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *namespaceGeneratingHandler) Remove(key string, obj *v1.Namespace) (*v1.Namespace, error) { if obj != nil { return obj, nil } obj = &v1.Namespace{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured NamespaceGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *namespaceGeneratingHandler) Handle(obj *v1.Namespace, status v1.NamespaceStatus) (v1.NamespaceStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.NamespaceGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *namespaceGeneratingHandler) isNewResourceVersion(obj *v1.Namespace) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *namespaceGeneratingHandler) storeResourceVersion(obj *v1.Namespace) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/node.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // NodeController interface for managing Node resources. type NodeController interface { generic.NonNamespacedControllerInterface[*v1.Node, *v1.NodeList] } // NodeClient interface for managing Node resources in Kubernetes. type NodeClient interface { generic.NonNamespacedClientInterface[*v1.Node, *v1.NodeList] } // NodeCache interface for retrieving Node resources in memory. type NodeCache interface { generic.NonNamespacedCacheInterface[*v1.Node] } // NodeStatusHandler is executed for every added or modified Node. Should return the new status to be updated type NodeStatusHandler func(obj *v1.Node, status v1.NodeStatus) (v1.NodeStatus, error) // NodeGeneratingHandler is the top-level handler that is executed for every Node event. It extends NodeStatusHandler by a returning a slice of child objects to be passed to apply.Apply type NodeGeneratingHandler func(obj *v1.Node, status v1.NodeStatus) ([]runtime.Object, v1.NodeStatus, error) // RegisterNodeStatusHandler configures a NodeController to execute a NodeStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterNodeStatusHandler(ctx context.Context, controller NodeController, condition condition.Cond, name string, handler NodeStatusHandler) { statusHandler := &nodeStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterNodeGeneratingHandler configures a NodeController to execute a NodeGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterNodeGeneratingHandler(ctx context.Context, controller NodeController, apply apply.Apply, condition condition.Cond, name string, handler NodeGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &nodeGeneratingHandler{ NodeGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterNodeStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type nodeStatusHandler struct { client NodeClient condition condition.Cond handler NodeStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *nodeStatusHandler) sync(key string, obj *v1.Node) (*v1.Node, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type nodeGeneratingHandler struct { NodeGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *nodeGeneratingHandler) Remove(key string, obj *v1.Node) (*v1.Node, error) { if obj != nil { return obj, nil } obj = &v1.Node{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured NodeGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *nodeGeneratingHandler) Handle(obj *v1.Node, status v1.NodeStatus) (v1.NodeStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.NodeGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *nodeGeneratingHandler) isNewResourceVersion(obj *v1.Node) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *nodeGeneratingHandler) storeResourceVersion(obj *v1.Node) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/persistentvolume.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // PersistentVolumeController interface for managing PersistentVolume resources. type PersistentVolumeController interface { generic.NonNamespacedControllerInterface[*v1.PersistentVolume, *v1.PersistentVolumeList] } // PersistentVolumeClient interface for managing PersistentVolume resources in Kubernetes. type PersistentVolumeClient interface { generic.NonNamespacedClientInterface[*v1.PersistentVolume, *v1.PersistentVolumeList] } // PersistentVolumeCache interface for retrieving PersistentVolume resources in memory. type PersistentVolumeCache interface { generic.NonNamespacedCacheInterface[*v1.PersistentVolume] } // PersistentVolumeStatusHandler is executed for every added or modified PersistentVolume. Should return the new status to be updated type PersistentVolumeStatusHandler func(obj *v1.PersistentVolume, status v1.PersistentVolumeStatus) (v1.PersistentVolumeStatus, error) // PersistentVolumeGeneratingHandler is the top-level handler that is executed for every PersistentVolume event. It extends PersistentVolumeStatusHandler by a returning a slice of child objects to be passed to apply.Apply type PersistentVolumeGeneratingHandler func(obj *v1.PersistentVolume, status v1.PersistentVolumeStatus) ([]runtime.Object, v1.PersistentVolumeStatus, error) // RegisterPersistentVolumeStatusHandler configures a PersistentVolumeController to execute a PersistentVolumeStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterPersistentVolumeStatusHandler(ctx context.Context, controller PersistentVolumeController, condition condition.Cond, name string, handler PersistentVolumeStatusHandler) { statusHandler := &persistentVolumeStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterPersistentVolumeGeneratingHandler configures a PersistentVolumeController to execute a PersistentVolumeGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterPersistentVolumeGeneratingHandler(ctx context.Context, controller PersistentVolumeController, apply apply.Apply, condition condition.Cond, name string, handler PersistentVolumeGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &persistentVolumeGeneratingHandler{ PersistentVolumeGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterPersistentVolumeStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type persistentVolumeStatusHandler struct { client PersistentVolumeClient condition condition.Cond handler PersistentVolumeStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *persistentVolumeStatusHandler) sync(key string, obj *v1.PersistentVolume) (*v1.PersistentVolume, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type persistentVolumeGeneratingHandler struct { PersistentVolumeGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *persistentVolumeGeneratingHandler) Remove(key string, obj *v1.PersistentVolume) (*v1.PersistentVolume, error) { if obj != nil { return obj, nil } obj = &v1.PersistentVolume{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured PersistentVolumeGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *persistentVolumeGeneratingHandler) Handle(obj *v1.PersistentVolume, status v1.PersistentVolumeStatus) (v1.PersistentVolumeStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.PersistentVolumeGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *persistentVolumeGeneratingHandler) isNewResourceVersion(obj *v1.PersistentVolume) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *persistentVolumeGeneratingHandler) storeResourceVersion(obj *v1.PersistentVolume) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/persistentvolumeclaim.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // PersistentVolumeClaimController interface for managing PersistentVolumeClaim resources. type PersistentVolumeClaimController interface { generic.ControllerInterface[*v1.PersistentVolumeClaim, *v1.PersistentVolumeClaimList] } // PersistentVolumeClaimClient interface for managing PersistentVolumeClaim resources in Kubernetes. type PersistentVolumeClaimClient interface { generic.ClientInterface[*v1.PersistentVolumeClaim, *v1.PersistentVolumeClaimList] } // PersistentVolumeClaimCache interface for retrieving PersistentVolumeClaim resources in memory. type PersistentVolumeClaimCache interface { generic.CacheInterface[*v1.PersistentVolumeClaim] } // PersistentVolumeClaimStatusHandler is executed for every added or modified PersistentVolumeClaim. Should return the new status to be updated type PersistentVolumeClaimStatusHandler func(obj *v1.PersistentVolumeClaim, status v1.PersistentVolumeClaimStatus) (v1.PersistentVolumeClaimStatus, error) // PersistentVolumeClaimGeneratingHandler is the top-level handler that is executed for every PersistentVolumeClaim event. It extends PersistentVolumeClaimStatusHandler by a returning a slice of child objects to be passed to apply.Apply type PersistentVolumeClaimGeneratingHandler func(obj *v1.PersistentVolumeClaim, status v1.PersistentVolumeClaimStatus) ([]runtime.Object, v1.PersistentVolumeClaimStatus, error) // RegisterPersistentVolumeClaimStatusHandler configures a PersistentVolumeClaimController to execute a PersistentVolumeClaimStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterPersistentVolumeClaimStatusHandler(ctx context.Context, controller PersistentVolumeClaimController, condition condition.Cond, name string, handler PersistentVolumeClaimStatusHandler) { statusHandler := &persistentVolumeClaimStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterPersistentVolumeClaimGeneratingHandler configures a PersistentVolumeClaimController to execute a PersistentVolumeClaimGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterPersistentVolumeClaimGeneratingHandler(ctx context.Context, controller PersistentVolumeClaimController, apply apply.Apply, condition condition.Cond, name string, handler PersistentVolumeClaimGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &persistentVolumeClaimGeneratingHandler{ PersistentVolumeClaimGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterPersistentVolumeClaimStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type persistentVolumeClaimStatusHandler struct { client PersistentVolumeClaimClient condition condition.Cond handler PersistentVolumeClaimStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *persistentVolumeClaimStatusHandler) sync(key string, obj *v1.PersistentVolumeClaim) (*v1.PersistentVolumeClaim, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type persistentVolumeClaimGeneratingHandler struct { PersistentVolumeClaimGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *persistentVolumeClaimGeneratingHandler) Remove(key string, obj *v1.PersistentVolumeClaim) (*v1.PersistentVolumeClaim, error) { if obj != nil { return obj, nil } obj = &v1.PersistentVolumeClaim{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured PersistentVolumeClaimGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *persistentVolumeClaimGeneratingHandler) Handle(obj *v1.PersistentVolumeClaim, status v1.PersistentVolumeClaimStatus) (v1.PersistentVolumeClaimStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.PersistentVolumeClaimGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *persistentVolumeClaimGeneratingHandler) isNewResourceVersion(obj *v1.PersistentVolumeClaim) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *persistentVolumeClaimGeneratingHandler) storeResourceVersion(obj *v1.PersistentVolumeClaim) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/pod.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // PodController interface for managing Pod resources. type PodController interface { generic.ControllerInterface[*v1.Pod, *v1.PodList] } // PodClient interface for managing Pod resources in Kubernetes. type PodClient interface { generic.ClientInterface[*v1.Pod, *v1.PodList] } // PodCache interface for retrieving Pod resources in memory. type PodCache interface { generic.CacheInterface[*v1.Pod] } // PodStatusHandler is executed for every added or modified Pod. Should return the new status to be updated type PodStatusHandler func(obj *v1.Pod, status v1.PodStatus) (v1.PodStatus, error) // PodGeneratingHandler is the top-level handler that is executed for every Pod event. It extends PodStatusHandler by a returning a slice of child objects to be passed to apply.Apply type PodGeneratingHandler func(obj *v1.Pod, status v1.PodStatus) ([]runtime.Object, v1.PodStatus, error) // RegisterPodStatusHandler configures a PodController to execute a PodStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterPodStatusHandler(ctx context.Context, controller PodController, condition condition.Cond, name string, handler PodStatusHandler) { statusHandler := &podStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterPodGeneratingHandler configures a PodController to execute a PodGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterPodGeneratingHandler(ctx context.Context, controller PodController, apply apply.Apply, condition condition.Cond, name string, handler PodGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &podGeneratingHandler{ PodGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterPodStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type podStatusHandler struct { client PodClient condition condition.Cond handler PodStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *podStatusHandler) sync(key string, obj *v1.Pod) (*v1.Pod, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type podGeneratingHandler struct { PodGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *podGeneratingHandler) Remove(key string, obj *v1.Pod) (*v1.Pod, error) { if obj != nil { return obj, nil } obj = &v1.Pod{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured PodGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *podGeneratingHandler) Handle(obj *v1.Pod, status v1.PodStatus) (v1.PodStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.PodGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *podGeneratingHandler) isNewResourceVersion(obj *v1.Pod) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *podGeneratingHandler) storeResourceVersion(obj *v1.Pod) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/resourcequota.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // ResourceQuotaController interface for managing ResourceQuota resources. type ResourceQuotaController interface { generic.ControllerInterface[*v1.ResourceQuota, *v1.ResourceQuotaList] } // ResourceQuotaClient interface for managing ResourceQuota resources in Kubernetes. type ResourceQuotaClient interface { generic.ClientInterface[*v1.ResourceQuota, *v1.ResourceQuotaList] } // ResourceQuotaCache interface for retrieving ResourceQuota resources in memory. type ResourceQuotaCache interface { generic.CacheInterface[*v1.ResourceQuota] } // ResourceQuotaStatusHandler is executed for every added or modified ResourceQuota. Should return the new status to be updated type ResourceQuotaStatusHandler func(obj *v1.ResourceQuota, status v1.ResourceQuotaStatus) (v1.ResourceQuotaStatus, error) // ResourceQuotaGeneratingHandler is the top-level handler that is executed for every ResourceQuota event. It extends ResourceQuotaStatusHandler by a returning a slice of child objects to be passed to apply.Apply type ResourceQuotaGeneratingHandler func(obj *v1.ResourceQuota, status v1.ResourceQuotaStatus) ([]runtime.Object, v1.ResourceQuotaStatus, error) // RegisterResourceQuotaStatusHandler configures a ResourceQuotaController to execute a ResourceQuotaStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterResourceQuotaStatusHandler(ctx context.Context, controller ResourceQuotaController, condition condition.Cond, name string, handler ResourceQuotaStatusHandler) { statusHandler := &resourceQuotaStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterResourceQuotaGeneratingHandler configures a ResourceQuotaController to execute a ResourceQuotaGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterResourceQuotaGeneratingHandler(ctx context.Context, controller ResourceQuotaController, apply apply.Apply, condition condition.Cond, name string, handler ResourceQuotaGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &resourceQuotaGeneratingHandler{ ResourceQuotaGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterResourceQuotaStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type resourceQuotaStatusHandler struct { client ResourceQuotaClient condition condition.Cond handler ResourceQuotaStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *resourceQuotaStatusHandler) sync(key string, obj *v1.ResourceQuota) (*v1.ResourceQuota, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type resourceQuotaGeneratingHandler struct { ResourceQuotaGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *resourceQuotaGeneratingHandler) Remove(key string, obj *v1.ResourceQuota) (*v1.ResourceQuota, error) { if obj != nil { return obj, nil } obj = &v1.ResourceQuota{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured ResourceQuotaGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *resourceQuotaGeneratingHandler) Handle(obj *v1.ResourceQuota, status v1.ResourceQuotaStatus) (v1.ResourceQuotaStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.ResourceQuotaGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *resourceQuotaGeneratingHandler) isNewResourceVersion(obj *v1.ResourceQuota) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *resourceQuotaGeneratingHandler) storeResourceVersion(obj *v1.ResourceQuota) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/secret.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/core/v1" ) // SecretController interface for managing Secret resources. type SecretController interface { generic.ControllerInterface[*v1.Secret, *v1.SecretList] } // SecretClient interface for managing Secret resources in Kubernetes. type SecretClient interface { generic.ClientInterface[*v1.Secret, *v1.SecretList] } // SecretCache interface for retrieving Secret resources in memory. type SecretCache interface { generic.CacheInterface[*v1.Secret] } ================================================ FILE: pkg/generated/controllers/core/v1/service.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // ServiceController interface for managing Service resources. type ServiceController interface { generic.ControllerInterface[*v1.Service, *v1.ServiceList] } // ServiceClient interface for managing Service resources in Kubernetes. type ServiceClient interface { generic.ClientInterface[*v1.Service, *v1.ServiceList] } // ServiceCache interface for retrieving Service resources in memory. type ServiceCache interface { generic.CacheInterface[*v1.Service] } // ServiceStatusHandler is executed for every added or modified Service. Should return the new status to be updated type ServiceStatusHandler func(obj *v1.Service, status v1.ServiceStatus) (v1.ServiceStatus, error) // ServiceGeneratingHandler is the top-level handler that is executed for every Service event. It extends ServiceStatusHandler by a returning a slice of child objects to be passed to apply.Apply type ServiceGeneratingHandler func(obj *v1.Service, status v1.ServiceStatus) ([]runtime.Object, v1.ServiceStatus, error) // RegisterServiceStatusHandler configures a ServiceController to execute a ServiceStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterServiceStatusHandler(ctx context.Context, controller ServiceController, condition condition.Cond, name string, handler ServiceStatusHandler) { statusHandler := &serviceStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterServiceGeneratingHandler configures a ServiceController to execute a ServiceGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterServiceGeneratingHandler(ctx context.Context, controller ServiceController, apply apply.Apply, condition condition.Cond, name string, handler ServiceGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &serviceGeneratingHandler{ ServiceGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterServiceStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type serviceStatusHandler struct { client ServiceClient condition condition.Cond handler ServiceStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *serviceStatusHandler) sync(key string, obj *v1.Service) (*v1.Service, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type serviceGeneratingHandler struct { ServiceGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *serviceGeneratingHandler) Remove(key string, obj *v1.Service) (*v1.Service, error) { if obj != nil { return obj, nil } obj = &v1.Service{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured ServiceGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *serviceGeneratingHandler) Handle(obj *v1.Service, status v1.ServiceStatus) (v1.ServiceStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.ServiceGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *serviceGeneratingHandler) isNewResourceVersion(obj *v1.Service) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *serviceGeneratingHandler) storeResourceVersion(obj *v1.Service) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/core/v1/serviceaccount.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/core/v1" ) // ServiceAccountController interface for managing ServiceAccount resources. type ServiceAccountController interface { generic.ControllerInterface[*v1.ServiceAccount, *v1.ServiceAccountList] } // ServiceAccountClient interface for managing ServiceAccount resources in Kubernetes. type ServiceAccountClient interface { generic.ClientInterface[*v1.ServiceAccount, *v1.ServiceAccountList] } // ServiceAccountCache interface for retrieving ServiceAccount resources in memory. type ServiceAccountCache interface { generic.CacheInterface[*v1.ServiceAccount] } ================================================ FILE: pkg/generated/controllers/discovery/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package discovery import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Discovery() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/discovery/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package discovery import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/discovery/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/discovery/v1/endpointslice.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/discovery/v1" ) // EndpointSliceController interface for managing EndpointSlice resources. type EndpointSliceController interface { generic.ControllerInterface[*v1.EndpointSlice, *v1.EndpointSliceList] } // EndpointSliceClient interface for managing EndpointSlice resources in Kubernetes. type EndpointSliceClient interface { generic.ClientInterface[*v1.EndpointSlice, *v1.EndpointSliceList] } // EndpointSliceCache interface for retrieving EndpointSlice resources in memory. type EndpointSliceCache interface { generic.CacheInterface[*v1.EndpointSlice] } ================================================ FILE: pkg/generated/controllers/discovery/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { EndpointSlice() EndpointSliceController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) EndpointSlice() EndpointSliceController { return generic.NewController[*v1.EndpointSlice, *v1.EndpointSliceList](schema.GroupVersionKind{Group: "discovery.k8s.io", Version: "v1", Kind: "EndpointSlice"}, "endpointslices", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/extensions/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package extensions import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Extensions() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/extensions/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package extensions import ( "github.com/rancher/lasso/pkg/controller" v1beta1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/extensions/v1beta1" ) type Interface interface { V1beta1() v1beta1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1beta1() v1beta1.Interface { return v1beta1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/extensions/v1beta1/ingress.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1beta1 import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/apply" "github.com/rancher/wrangler/v3/pkg/condition" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" v1beta1 "k8s.io/api/extensions/v1beta1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // IngressController interface for managing Ingress resources. type IngressController interface { generic.ControllerInterface[*v1beta1.Ingress, *v1beta1.IngressList] } // IngressClient interface for managing Ingress resources in Kubernetes. type IngressClient interface { generic.ClientInterface[*v1beta1.Ingress, *v1beta1.IngressList] } // IngressCache interface for retrieving Ingress resources in memory. type IngressCache interface { generic.CacheInterface[*v1beta1.Ingress] } // IngressStatusHandler is executed for every added or modified Ingress. Should return the new status to be updated type IngressStatusHandler func(obj *v1beta1.Ingress, status v1beta1.IngressStatus) (v1beta1.IngressStatus, error) // IngressGeneratingHandler is the top-level handler that is executed for every Ingress event. It extends IngressStatusHandler by a returning a slice of child objects to be passed to apply.Apply type IngressGeneratingHandler func(obj *v1beta1.Ingress, status v1beta1.IngressStatus) ([]runtime.Object, v1beta1.IngressStatus, error) // RegisterIngressStatusHandler configures a IngressController to execute a IngressStatusHandler for every events observed. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterIngressStatusHandler(ctx context.Context, controller IngressController, condition condition.Cond, name string, handler IngressStatusHandler) { statusHandler := &ingressStatusHandler{ client: controller, condition: condition, handler: handler, } controller.AddGenericHandler(ctx, name, generic.FromObjectHandlerToHandler(statusHandler.sync)) } // RegisterIngressGeneratingHandler configures a IngressController to execute a IngressGeneratingHandler for every events observed, passing the returned objects to the provided apply.Apply. // If a non-empty condition is provided, it will be updated in the status conditions for every handler execution func RegisterIngressGeneratingHandler(ctx context.Context, controller IngressController, apply apply.Apply, condition condition.Cond, name string, handler IngressGeneratingHandler, opts *generic.GeneratingHandlerOptions) { statusHandler := &ingressGeneratingHandler{ IngressGeneratingHandler: handler, apply: apply, name: name, gvk: controller.GroupVersionKind(), } if opts != nil { statusHandler.opts = *opts } controller.OnChange(ctx, name, statusHandler.Remove) RegisterIngressStatusHandler(ctx, controller, condition, name, statusHandler.Handle) } type ingressStatusHandler struct { client IngressClient condition condition.Cond handler IngressStatusHandler } // sync is executed on every resource addition or modification. Executes the configured handlers and sends the updated status to the Kubernetes API func (a *ingressStatusHandler) sync(key string, obj *v1beta1.Ingress) (*v1beta1.Ingress, error) { if obj == nil { return obj, nil } origStatus := obj.Status.DeepCopy() obj = obj.DeepCopy() newStatus, err := a.handler(obj, obj.Status) if err != nil { // Revert to old status on error newStatus = *origStatus.DeepCopy() } if a.condition != "" { if errors.IsConflict(err) { a.condition.SetError(&newStatus, "", nil) } else { a.condition.SetError(&newStatus, "", err) } } if !equality.Semantic.DeepEqual(origStatus, &newStatus) { if a.condition != "" { // Since status has changed, update the lastUpdatedTime a.condition.LastUpdated(&newStatus, time.Now().UTC().Format(time.RFC3339)) } var newErr error obj.Status = newStatus newObj, newErr := a.client.UpdateStatus(obj) if err == nil { err = newErr } if newErr == nil { obj = newObj } } return obj, err } type ingressGeneratingHandler struct { IngressGeneratingHandler apply apply.Apply opts generic.GeneratingHandlerOptions gvk schema.GroupVersionKind name string seen sync.Map } // Remove handles the observed deletion of a resource, cascade deleting every associated resource previously applied func (a *ingressGeneratingHandler) Remove(key string, obj *v1beta1.Ingress) (*v1beta1.Ingress, error) { if obj != nil { return obj, nil } obj = &v1beta1.Ingress{} obj.Namespace, obj.Name = kv.RSplit(key, "/") obj.SetGroupVersionKind(a.gvk) if a.opts.UniqueApplyForResourceVersion { a.seen.Delete(key) } return nil, generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects() } // Handle executes the configured IngressGeneratingHandler and pass the resulting objects to apply.Apply, finally returning the new status of the resource func (a *ingressGeneratingHandler) Handle(obj *v1beta1.Ingress, status v1beta1.IngressStatus) (v1beta1.IngressStatus, error) { if !obj.DeletionTimestamp.IsZero() { return status, nil } objs, newStatus, err := a.IngressGeneratingHandler(obj, status) if err != nil { return newStatus, err } if !a.isNewResourceVersion(obj) { return newStatus, nil } err = generic.ConfigureApplyForObject(a.apply, obj, &a.opts). WithOwner(obj). WithSetID(a.name). ApplyObjects(objs...) if err != nil { return newStatus, err } a.storeResourceVersion(obj) return newStatus, nil } // isNewResourceVersion detects if a specific resource version was already successfully processed. // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *ingressGeneratingHandler) isNewResourceVersion(obj *v1beta1.Ingress) bool { if !a.opts.UniqueApplyForResourceVersion { return true } // Apply once per resource version key := obj.Namespace + "/" + obj.Name previous, ok := a.seen.Load(key) return !ok || previous != obj.ResourceVersion } // storeResourceVersion keeps track of the latest resource version of an object for which Apply was executed // Only used if UniqueApplyForResourceVersion is set in generic.GeneratingHandlerOptions func (a *ingressGeneratingHandler) storeResourceVersion(obj *v1beta1.Ingress) { if !a.opts.UniqueApplyForResourceVersion { return } key := obj.Namespace + "/" + obj.Name a.seen.Store(key, obj.ResourceVersion) } ================================================ FILE: pkg/generated/controllers/extensions/v1beta1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1beta1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1beta1 "k8s.io/api/extensions/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1beta1.AddToScheme) } type Interface interface { Ingress() IngressController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) Ingress() IngressController { return generic.NewController[*v1beta1.Ingress, *v1beta1.IngressList](schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"}, "ingresses", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/networking.k8s.io/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package networking import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Networking() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/networking.k8s.io/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package networking import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/networking.k8s.io/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/networking.k8s.io/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { NetworkPolicy() NetworkPolicyController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) NetworkPolicy() NetworkPolicyController { return generic.NewController[*v1.NetworkPolicy, *v1.NetworkPolicyList](schema.GroupVersionKind{Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}, "networkpolicies", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/networking.k8s.io/v1/networkpolicy.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/networking/v1" ) // NetworkPolicyController interface for managing NetworkPolicy resources. type NetworkPolicyController interface { generic.ControllerInterface[*v1.NetworkPolicy, *v1.NetworkPolicyList] } // NetworkPolicyClient interface for managing NetworkPolicy resources in Kubernetes. type NetworkPolicyClient interface { generic.ClientInterface[*v1.NetworkPolicy, *v1.NetworkPolicyList] } // NetworkPolicyCache interface for retrieving NetworkPolicy resources in memory. type NetworkPolicyCache interface { generic.CacheInterface[*v1.NetworkPolicy] } ================================================ FILE: pkg/generated/controllers/rbac/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package rbac import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Rbac() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/rbac/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package rbac import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/rbac/v1/clusterrole.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/rbac/v1" ) // ClusterRoleController interface for managing ClusterRole resources. type ClusterRoleController interface { generic.NonNamespacedControllerInterface[*v1.ClusterRole, *v1.ClusterRoleList] } // ClusterRoleClient interface for managing ClusterRole resources in Kubernetes. type ClusterRoleClient interface { generic.NonNamespacedClientInterface[*v1.ClusterRole, *v1.ClusterRoleList] } // ClusterRoleCache interface for retrieving ClusterRole resources in memory. type ClusterRoleCache interface { generic.NonNamespacedCacheInterface[*v1.ClusterRole] } ================================================ FILE: pkg/generated/controllers/rbac/v1/clusterrolebinding.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/rbac/v1" ) // ClusterRoleBindingController interface for managing ClusterRoleBinding resources. type ClusterRoleBindingController interface { generic.NonNamespacedControllerInterface[*v1.ClusterRoleBinding, *v1.ClusterRoleBindingList] } // ClusterRoleBindingClient interface for managing ClusterRoleBinding resources in Kubernetes. type ClusterRoleBindingClient interface { generic.NonNamespacedClientInterface[*v1.ClusterRoleBinding, *v1.ClusterRoleBindingList] } // ClusterRoleBindingCache interface for retrieving ClusterRoleBinding resources in memory. type ClusterRoleBindingCache interface { generic.NonNamespacedCacheInterface[*v1.ClusterRoleBinding] } ================================================ FILE: pkg/generated/controllers/rbac/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { ClusterRole() ClusterRoleController ClusterRoleBinding() ClusterRoleBindingController Role() RoleController RoleBinding() RoleBindingController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) ClusterRole() ClusterRoleController { return generic.NewNonNamespacedController[*v1.ClusterRole, *v1.ClusterRoleList](schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, "clusterroles", v.controllerFactory) } func (v *version) ClusterRoleBinding() ClusterRoleBindingController { return generic.NewNonNamespacedController[*v1.ClusterRoleBinding, *v1.ClusterRoleBindingList](schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, "clusterrolebindings", v.controllerFactory) } func (v *version) Role() RoleController { return generic.NewController[*v1.Role, *v1.RoleList](schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, "roles", true, v.controllerFactory) } func (v *version) RoleBinding() RoleBindingController { return generic.NewController[*v1.RoleBinding, *v1.RoleBindingList](schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}, "rolebindings", true, v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/rbac/v1/role.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/rbac/v1" ) // RoleController interface for managing Role resources. type RoleController interface { generic.ControllerInterface[*v1.Role, *v1.RoleList] } // RoleClient interface for managing Role resources in Kubernetes. type RoleClient interface { generic.ClientInterface[*v1.Role, *v1.RoleList] } // RoleCache interface for retrieving Role resources in memory. type RoleCache interface { generic.CacheInterface[*v1.Role] } ================================================ FILE: pkg/generated/controllers/rbac/v1/rolebinding.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/rbac/v1" ) // RoleBindingController interface for managing RoleBinding resources. type RoleBindingController interface { generic.ControllerInterface[*v1.RoleBinding, *v1.RoleBindingList] } // RoleBindingClient interface for managing RoleBinding resources in Kubernetes. type RoleBindingClient interface { generic.ClientInterface[*v1.RoleBinding, *v1.RoleBindingList] } // RoleBindingCache interface for retrieving RoleBinding resources in memory. type RoleBindingCache interface { generic.CacheInterface[*v1.RoleBinding] } ================================================ FILE: pkg/generated/controllers/storage/factory.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package storage import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "k8s.io/client-go/rest" ) type Factory struct { *generic.Factory } func NewFactoryFromConfigOrDie(config *rest.Config) *Factory { f, err := NewFactoryFromConfig(config) if err != nil { panic(err) } return f } func NewFactoryFromConfig(config *rest.Config) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, nil) } func NewFactoryFromConfigWithNamespace(config *rest.Config, namespace string) (*Factory, error) { return NewFactoryFromConfigWithOptions(config, &FactoryOptions{ Namespace: namespace, }) } type FactoryOptions = generic.FactoryOptions func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { f, err := generic.NewFactoryFromConfigWithOptions(config, opts) return &Factory{ Factory: f, }, err } func NewFactoryFromConfigWithOptionsOrDie(config *rest.Config, opts *FactoryOptions) *Factory { f, err := NewFactoryFromConfigWithOptions(config, opts) if err != nil { panic(err) } return f } func (c *Factory) Storage() Interface { return New(c.ControllerFactory()) } func (c *Factory) WithAgent(userAgent string) Interface { return New(controller.NewSharedControllerFactoryWithAgent(userAgent, c.ControllerFactory())) } ================================================ FILE: pkg/generated/controllers/storage/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package storage import ( "github.com/rancher/lasso/pkg/controller" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/storage/v1" ) type Interface interface { V1() v1.Interface } type group struct { controllerFactory controller.SharedControllerFactory } // New returns a new Interface. func New(controllerFactory controller.SharedControllerFactory) Interface { return &group{ controllerFactory: controllerFactory, } } func (g *group) V1() v1.Interface { return v1.New(g.controllerFactory) } ================================================ FILE: pkg/generated/controllers/storage/v1/interface.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/schemes" v1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func init() { schemes.Register(v1.AddToScheme) } type Interface interface { StorageClass() StorageClassController } func New(controllerFactory controller.SharedControllerFactory) Interface { return &version{ controllerFactory: controllerFactory, } } type version struct { controllerFactory controller.SharedControllerFactory } func (v *version) StorageClass() StorageClassController { return generic.NewNonNamespacedController[*v1.StorageClass, *v1.StorageClassList](schema.GroupVersionKind{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}, "storageclasses", v.controllerFactory) } ================================================ FILE: pkg/generated/controllers/storage/v1/storageclass.go ================================================ /* Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Code generated by main. DO NOT EDIT. package v1 import ( "github.com/rancher/wrangler/v3/pkg/generic" v1 "k8s.io/api/storage/v1" ) // StorageClassController interface for managing StorageClass resources. type StorageClassController interface { generic.NonNamespacedControllerInterface[*v1.StorageClass, *v1.StorageClassList] } // StorageClassClient interface for managing StorageClass resources in Kubernetes. type StorageClassClient interface { generic.NonNamespacedClientInterface[*v1.StorageClass, *v1.StorageClassList] } // StorageClassCache interface for retrieving StorageClass resources in memory. type StorageClassCache interface { generic.NonNamespacedCacheInterface[*v1.StorageClass] } ================================================ FILE: pkg/generic/cache.go ================================================ package generic import ( "fmt" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/tools/cache" ) // CacheInterface is an interface for Object retrieval from memory. type CacheInterface[T runtime.Object] interface { // Get returns the resources with the specified name in the given namespace from the cache. Get(namespace, name string) (T, error) // List will attempt to find resources in the given namespace from the Cache. List(namespace string, selector labels.Selector) ([]T, error) // AddIndexer adds a new Indexer to the cache with the provided name. // If you call this after you already have data in the store, the results are undefined. AddIndexer(indexName string, indexer Indexer[T]) // GetByIndex returns the stored objects whose set of indexed values // for the named index includes the given indexed value. GetByIndex(indexName, key string) ([]T, error) } // NonNamespacedCacheInterface is an interface for non namespaced Object retrieval from memory. type NonNamespacedCacheInterface[T runtime.Object] interface { // Get returns the resources with the specified name from the cache. Get(name string) (T, error) // List will attempt to find resources from the Cache. List(selector labels.Selector) ([]T, error) // AddIndexer adds a new Indexer to the cache with the provided name. // If you call this after you already have data in the store, the results are undefined. AddIndexer(indexName string, indexer Indexer[T]) // GetByIndex returns the stored objects whose set of indexed values // for the named index includes the given indexed value. GetByIndex(indexName, key string) ([]T, error) } func NewCache[T runtime.Object](indexer cache.Indexer, resource schema.GroupResource) *Cache[T] { return &Cache[T]{ indexer: indexer, resource: resource, } } func NewNonNamespacedCache[T runtime.Object](indexer cache.Indexer, resource schema.GroupResource) *NonNamespacedCache[T] { return &NonNamespacedCache[T]{ CacheInterface: &Cache[T]{ indexer: indexer, resource: resource, }, } } // Cache is a object cache stored in memory for objects of type T. type Cache[T runtime.Object] struct { indexer cache.Indexer resource schema.GroupResource } // NonNamespacedCache is a Cache for objects of type T that are not namespaced. type NonNamespacedCache[T runtime.Object] struct { CacheInterface[T] } // Get returns the resources with the specified name in the given namespace from the cache. func (c *Cache[T]) Get(namespace, name string) (T, error) { var nilObj T key := name if namespace != metav1.NamespaceAll { key = namespace + "/" + key } obj, exists, err := c.indexer.GetByKey(key) if err != nil { return nilObj, err } if !exists { return nilObj, errors.NewNotFound(c.resource, name) } ret, ok := obj.(T) if !ok { return ret, fmt.Errorf("could not convert cache item to %T", *new(T)) } return ret, nil } // List will attempt to find resources in the given namespace from the Cache. func (c *Cache[T]) List(namespace string, selector labels.Selector) (ret []T, err error) { err = cache.ListAllByNamespace(c.indexer, namespace, selector, func(m interface{}) { ret = append(ret, m.(T)) }) return ret, err } // AddIndexer adds a new Indexer to the cache with the provided name. // If you call this after you already have data in the store, the results are undefined. func (c *Cache[T]) AddIndexer(indexName string, indexer Indexer[T]) { utilruntime.Must(c.indexer.AddIndexers(map[string]cache.IndexFunc{ indexName: func(obj interface{}) (strings []string, e error) { return indexer(obj.(T)) }, })) } // GetByIndex returns the stored objects whose set of indexed values // for the named index includes the given indexed value. func (c *Cache[T]) GetByIndex(indexName, key string) (result []T, err error) { objs, err := c.indexer.ByIndex(indexName, key) if err != nil { return nil, err } result = make([]T, 0, len(objs)) for _, obj := range objs { ret, ok := obj.(T) if !ok { return nil, fmt.Errorf("could not convert cache item to %T", *new(T)) } result = append(result, ret) } return result, nil } // Get calls Cache.Get(...) with an empty namespace parameter. func (c *NonNamespacedCache[T]) Get(name string) (T, error) { return c.CacheInterface.Get(metav1.NamespaceAll, name) } // Get calls Cache.List(...) with an empty namespace parameter. func (c *NonNamespacedCache[T]) List(selector labels.Selector) (ret []T, err error) { return c.CacheInterface.List(metav1.NamespaceAll, selector) } ================================================ FILE: pkg/generic/cache_test.go ================================================ package generic import ( "testing" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" ) func TestCache(t *testing.T) { indexer := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, nil) indexer.Add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: "test-01"}}) // test cache with correct type for indexer contents podCache := NewCache[*v1.Pod](indexer, v1.SchemeGroupVersion.WithResource("pods").GroupResource()) if _, err := podCache.Get(metav1.NamespaceDefault, "test-01"); err != nil { t.Fatalf("failed to get pod: %v", err) } if _, err := podCache.Get(metav1.NamespaceSystem, "test-01"); err == nil { t.Fatalf("unexpected success getting nonexistent pod") } if _, err := podCache.Get(metav1.NamespaceDefault, "test-02"); err == nil { t.Fatalf("unexpected success getting nonexistent pod") } // test cache with wrong type for indexer contents secretCache := NewCache[*v1.Secret](indexer, v1.SchemeGroupVersion.WithResource("secrets").GroupResource()) if _, err := secretCache.Get(metav1.NamespaceDefault, "test-01"); err == nil { t.Fatalf("unexpected success getting secret from pod indexer") } if _, err := secretCache.Get(metav1.NamespaceSystem, "test-01"); err == nil { t.Fatalf("unexpected success getting secret from pod indexer") } if _, err := secretCache.Get(metav1.NamespaceDefault, "test-02"); err == nil { t.Fatalf("unexpected success getting secret from pod indexer") } } func TestNonNamespacedCache(t *testing.T) { indexer := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, nil) indexer.Add(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-01"}}) // test cache with correct type for indexer contents nodeCache := NewNonNamespacedCache[*v1.Node](indexer, v1.SchemeGroupVersion.WithResource("nodes").GroupResource()) if _, err := nodeCache.Get("test-01"); err != nil { t.Fatalf("failed to get node: %v", err) } if _, err := nodeCache.Get("test-02"); err == nil { t.Fatalf("unexpected success getting nonexistent node") } // test cache with wrong type for indexer contents pvCache := NewNonNamespacedCache[*v1.PersistentVolume](indexer, v1.SchemeGroupVersion.WithResource("persistentvolumes").GroupResource()) if _, err := pvCache.Get("test-01"); err == nil { t.Fatalf("unexpected success getting pv from node indexer") } if _, err := pvCache.Get("test-02"); err == nil { t.Fatalf("unexpected success getting pv from node indexer") } } ================================================ FILE: pkg/generic/clientMocks_test.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: ./embeddedClient.go // // Generated by this command: // // mockgen -package generic -destination ./clientMocks_test.go -source ./embeddedClient.go // // Package generic is a generated GoMock package. package generic import ( context "context" reflect "reflect" client "github.com/rancher/lasso/pkg/client" gomock "go.uber.org/mock/gomock" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" rest "k8s.io/client-go/rest" ) // MockembeddedClient is a mock of embeddedClient interface. type MockembeddedClient struct { ctrl *gomock.Controller recorder *MockembeddedClientMockRecorder isgomock struct{} } // MockembeddedClientMockRecorder is the mock recorder for MockembeddedClient. type MockembeddedClientMockRecorder struct { mock *MockembeddedClient } // NewMockembeddedClient creates a new mock instance. func NewMockembeddedClient(ctrl *gomock.Controller) *MockembeddedClient { mock := &MockembeddedClient{ctrl: ctrl} mock.recorder = &MockembeddedClientMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockembeddedClient) EXPECT() *MockembeddedClientMockRecorder { return m.recorder } // Create mocks base method. func (m *MockembeddedClient) Create(ctx context.Context, namespace string, obj, result runtime.Object, opts v1.CreateOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", ctx, namespace, obj, result, opts) ret0, _ := ret[0].(error) return ret0 } // Create indicates an expected call of Create. func (mr *MockembeddedClientMockRecorder) Create(ctx, namespace, obj, result, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockembeddedClient)(nil).Create), ctx, namespace, obj, result, opts) } // Delete mocks base method. func (m *MockembeddedClient) Delete(ctx context.Context, namespace, name string, opts v1.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", ctx, namespace, name, opts) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockembeddedClientMockRecorder) Delete(ctx, namespace, name, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockembeddedClient)(nil).Delete), ctx, namespace, name, opts) } // DeleteCollection mocks base method. func (m *MockembeddedClient) DeleteCollection(ctx context.Context, namespace string, opts v1.DeleteOptions, listOpts v1.ListOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteCollection", ctx, namespace, opts, listOpts) ret0, _ := ret[0].(error) return ret0 } // DeleteCollection indicates an expected call of DeleteCollection. func (mr *MockembeddedClientMockRecorder) DeleteCollection(ctx, namespace, opts, listOpts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockembeddedClient)(nil).DeleteCollection), ctx, namespace, opts, listOpts) } // Get mocks base method. func (m *MockembeddedClient) Get(ctx context.Context, namespace, name string, result runtime.Object, options v1.GetOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ctx, namespace, name, result, options) ret0, _ := ret[0].(error) return ret0 } // Get indicates an expected call of Get. func (mr *MockembeddedClientMockRecorder) Get(ctx, namespace, name, result, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockembeddedClient)(nil).Get), ctx, namespace, name, result, options) } // List mocks base method. func (m *MockembeddedClient) List(ctx context.Context, namespace string, result runtime.Object, opts v1.ListOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", ctx, namespace, result, opts) ret0, _ := ret[0].(error) return ret0 } // List indicates an expected call of List. func (mr *MockembeddedClientMockRecorder) List(ctx, namespace, result, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockembeddedClient)(nil).List), ctx, namespace, result, opts) } // Patch mocks base method. func (m *MockembeddedClient) Patch(ctx context.Context, namespace, name string, pt types.PatchType, data []byte, result runtime.Object, opts v1.PatchOptions, subresources ...string) error { m.ctrl.T.Helper() varargs := []any{ctx, namespace, name, pt, data, result, opts} for _, a := range subresources { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Patch", varargs...) ret0, _ := ret[0].(error) return ret0 } // Patch indicates an expected call of Patch. func (mr *MockembeddedClientMockRecorder) Patch(ctx, namespace, name, pt, data, result, opts any, subresources ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{ctx, namespace, name, pt, data, result, opts}, subresources...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockembeddedClient)(nil).Patch), varargs...) } // Update mocks base method. func (m *MockembeddedClient) Update(ctx context.Context, namespace string, obj, result runtime.Object, opts v1.UpdateOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", ctx, namespace, obj, result, opts) ret0, _ := ret[0].(error) return ret0 } // Update indicates an expected call of Update. func (mr *MockembeddedClientMockRecorder) Update(ctx, namespace, obj, result, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockembeddedClient)(nil).Update), ctx, namespace, obj, result, opts) } // UpdateStatus mocks base method. func (m *MockembeddedClient) UpdateStatus(ctx context.Context, namespace string, obj, result runtime.Object, opts v1.UpdateOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStatus", ctx, namespace, obj, result, opts) ret0, _ := ret[0].(error) return ret0 } // UpdateStatus indicates an expected call of UpdateStatus. func (mr *MockembeddedClientMockRecorder) UpdateStatus(ctx, namespace, obj, result, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockembeddedClient)(nil).UpdateStatus), ctx, namespace, obj, result, opts) } // Watch mocks base method. func (m *MockembeddedClient) Watch(ctx context.Context, namespace string, opts v1.ListOptions) (watch.Interface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Watch", ctx, namespace, opts) ret0, _ := ret[0].(watch.Interface) ret1, _ := ret[1].(error) return ret0, ret1 } // Watch indicates an expected call of Watch. func (mr *MockembeddedClientMockRecorder) Watch(ctx, namespace, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockembeddedClient)(nil).Watch), ctx, namespace, opts) } // WithImpersonation mocks base method. func (m *MockembeddedClient) WithImpersonation(impersonate rest.ImpersonationConfig) (*client.Client, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithImpersonation", impersonate) ret0, _ := ret[0].(*client.Client) ret1, _ := ret[1].(error) return ret0, ret1 } // WithImpersonation indicates an expected call of WithImpersonation. func (mr *MockembeddedClientMockRecorder) WithImpersonation(impersonate any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithImpersonation", reflect.TypeOf((*MockembeddedClient)(nil).WithImpersonation), impersonate) } ================================================ FILE: pkg/generic/controller.go ================================================ // Package generic provides generic types and implementations for Controllers, Clients, and Caches. package generic import ( "context" "fmt" "reflect" "time" "github.com/rancher/lasso/pkg/controller" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" ) // ErrSkip notifies the caller to skip this error. var ErrSkip = controller.ErrIgnore // ControllerMeta holds meta information shared by all controllers. type ControllerMeta interface { // Informer returns the SharedIndexInformer used by this controller. Informer() cache.SharedIndexInformer // GroupVersionKind returns the GVK used to create this Controller. GroupVersionKind() schema.GroupVersionKind // AddGenericHandler adds a generic handler that runs when a resource changes. AddGenericHandler(ctx context.Context, name string, handler Handler) // AddGenericHandler adds a generic handler that runs when a resource is removed. AddGenericRemoveHandler(ctx context.Context, name string, handler Handler) // Updater returns a update function that will attempt to perform an update for a specific resource type. Updater() Updater } // RuntimeMetaObject is an interface for a K8s Object to be used with a specific controller. type RuntimeMetaObject interface { comparable runtime.Object metav1.Object } // ControllerInterface interface for managing K8s Objects. type ControllerInterface[T RuntimeMetaObject, TList runtime.Object] interface { ControllerMeta ClientInterface[T, TList] // OnChange runs the given object handler when the controller detects a resource was changed. OnChange(ctx context.Context, name string, sync ObjectHandler[T]) // OnRemove runs the given object handler when the controller detects a resource was changed. OnRemove(ctx context.Context, name string, sync ObjectHandler[T]) // Enqueue adds the resource with the given name in the provided namespace to the worker queue of the controller. Enqueue(namespace, name string) // EnqueueAfter runs Enqueue after the provided duration. EnqueueAfter(namespace, name string, duration time.Duration) // Cache returns a cache for the resource type T. Cache() CacheInterface[T] } // NonNamespacedControllerInterface interface for managing non namespaced K8s Objects. type NonNamespacedControllerInterface[T RuntimeMetaObject, TList runtime.Object] interface { ControllerMeta NonNamespacedClientInterface[T, TList] // OnChange runs the given object handler when the controller detects a resource was changed. OnChange(ctx context.Context, name string, sync ObjectHandler[T]) // OnRemove runs the given object handler when the controller detects a resource was changed. OnRemove(ctx context.Context, name string, sync ObjectHandler[T]) // Enqueue adds the resource with the given name to the worker queue of the controller. Enqueue(name string) // EnqueueAfter runs Enqueue after the provided duration. EnqueueAfter(name string, duration time.Duration) // Cache returns a cache for the resource type T. Cache() NonNamespacedCacheInterface[T] } // ClientInterface is an interface to performs CRUD like operations on an Objects. type ClientInterface[T RuntimeMetaObject, TList runtime.Object] interface { // Create creates a new object and return the newly created Object or an error. Create(T) (T, error) // Update updates the object and return the newly updated Object or an error. Update(T) (T, error) // UpdateStatus updates the Status field of a the object and return the newly updated Object or an error. // Will always return an error if the object does not have a status field. UpdateStatus(T) (T, error) // Delete deletes the Object in the given name and namespace. Delete(namespace, name string, options *metav1.DeleteOptions) error // Get will attempt to retrieve the resource with the given name in the given namespace. Get(namespace, name string, options metav1.GetOptions) (T, error) // List will attempt to find resources in the given namespace. List(namespace string, opts metav1.ListOptions) (TList, error) // Watch will start watching resources in the given namespace. Watch(namespace string, opts metav1.ListOptions) (watch.Interface, error) // Patch will patch the resource with the matching name in the matching namespace. Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (result T, err error) // WithImpersonation returns a new copy of the client that uses impersonation. WithImpersonation(impersonate rest.ImpersonationConfig) (ClientInterface[T, TList], error) // DeleteCollection deletes all resources matching the ListOptions in the // provided namespace. DeleteCollection(namespace string, deleteOpts metav1.DeleteOptions, listOpts metav1.ListOptions) error } // NonNamespacedClientInterface is an interface to performs CRUD like operations on nonNamespaced Objects. type NonNamespacedClientInterface[T RuntimeMetaObject, TList runtime.Object] interface { // Create creates a new object and return the newly created Object or an error. Create(T) (T, error) // Update updates the object and return the newly updated Object or an error. Update(T) (T, error) // UpdateStatus updates the Status field of a the object and return the newly updated Object or an error. // Will always return an error if the object does not have a status field. UpdateStatus(T) (T, error) // Delete deletes the Object in the given name. Delete(name string, options *metav1.DeleteOptions) error // Get will attempt to retrieve the resource with the specified name. Get(name string, options metav1.GetOptions) (T, error) // List will attempt to find multiple resources. List(opts metav1.ListOptions) (TList, error) // Watch will start watching resources. Watch(opts metav1.ListOptions) (watch.Interface, error) // Patch will patch the resource with the matching name. Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result T, err error) // WithImpersonation returns a new copy of the client that uses impersonation. WithImpersonation(impersonate rest.ImpersonationConfig) (NonNamespacedClientInterface[T, TList], error) } // ObjectHandler performs operations on the given runtime.Object and returns the new runtime.Object or an error type Handler func(key string, obj runtime.Object) (runtime.Object, error) // ObjectHandler performs operations on the given object and returns the new object or an error type ObjectHandler[T runtime.Object] func(string, T) (T, error) // Indexer computes a set of indexed values for the provided object. type Indexer[T runtime.Object] func(obj T) ([]string, error) // FromObjectHandlerToHandler converts an ObjecHandler to a Handler. func FromObjectHandlerToHandler[T RuntimeMetaObject](sync ObjectHandler[T]) Handler { return func(key string, obj runtime.Object) (runtime.Object, error) { var nilObj, retObj T var err error if obj == nil { retObj, err = sync(key, nilObj) } else { retObj, err = sync(key, obj.(T)) } if retObj == nilObj { return nil, err } return retObj, err } } // Controller is used to manage objects of type T. type Controller[T RuntimeMetaObject, TList runtime.Object] struct { controller controller.SharedController embeddedClient gvk schema.GroupVersionKind groupResource schema.GroupResource objType reflect.Type objListType reflect.Type } // NonNamespacedController is a Controller for non namespaced resources. This controller provides similar function definitions as Controller except the namespace parameter is omitted. type NonNamespacedController[T RuntimeMetaObject, TList runtime.Object] struct { *Controller[T, TList] } // NewController creates a new controller for the given Object type and ObjectList type. func NewController[T RuntimeMetaObject, TList runtime.Object](gvk schema.GroupVersionKind, resource string, namespaced bool, controller controller.SharedControllerFactory) *Controller[T, TList] { sharedCtrl := controller.ForResourceKind(gvk.GroupVersion().WithResource(resource), gvk.Kind, namespaced) var obj T objPtrType := reflect.TypeOf(obj) if objPtrType.Kind() != reflect.Pointer { panic(fmt.Sprintf("Controller requires Object T to be a pointer not %v", objPtrType)) } var objList TList objListPtrType := reflect.TypeOf(objList) if objListPtrType.Kind() != reflect.Pointer { panic(fmt.Sprintf("Controller requires Object TList to be a pointer not %v", objListPtrType)) } return &Controller[T, TList]{ controller: sharedCtrl, embeddedClient: sharedCtrl.Client(), gvk: gvk, groupResource: schema.GroupResource{ Group: gvk.Group, Resource: resource, }, objType: objPtrType.Elem(), objListType: objListPtrType.Elem(), } } // Updater creates a new Updater for the Object type T. func (c *Controller[T, TList]) Updater() Updater { var nilObj T return func(obj runtime.Object) (runtime.Object, error) { newObj, err := c.Update(obj.(T)) if newObj == nilObj { return nil, err } return newObj, err } } // AddGenericHandler runs the given handler when the controller detects an object was changed. func (c *Controller[T, TList]) AddGenericHandler(ctx context.Context, name string, handler Handler) { c.controller.RegisterHandler(ctx, name, controller.SharedControllerHandlerFunc(handler)) } // AddGenericRemoveHandler runs the given handler when the controller detects an object was removed. func (c *Controller[T, TList]) AddGenericRemoveHandler(ctx context.Context, name string, handler Handler) { c.AddGenericHandler(ctx, name, NewRemoveHandler(name, c.Updater(), handler)) } // OnChange runs the given object handler when the controller detects a resource was changed. func (c *Controller[T, TList]) OnChange(ctx context.Context, name string, sync ObjectHandler[T]) { c.AddGenericHandler(ctx, name, FromObjectHandlerToHandler(sync)) } // OnRemove runs the given object handler when the controller detects a resource was changed. func (c *Controller[T, TList]) OnRemove(ctx context.Context, name string, sync ObjectHandler[T]) { c.AddGenericHandler(ctx, name, NewRemoveHandler(name, c.Updater(), FromObjectHandlerToHandler(sync))) } // Enqueue adds the resource with the given name in the provided namespace to the worker queue of the controller. func (c *Controller[T, TList]) Enqueue(namespace, name string) { c.controller.Enqueue(namespace, name) } // EnqueueAfter runs Enqueue after the provided duration. func (c *Controller[T, TList]) EnqueueAfter(namespace, name string, duration time.Duration) { c.controller.EnqueueAfter(namespace, name, duration) } // Informer returns the SharedIndexInformer used by this controller. func (c *Controller[T, TList]) Informer() cache.SharedIndexInformer { return c.controller.Informer() } // GroupVersionKind returns the GVK used to create this Controller. func (c *Controller[T, TList]) GroupVersionKind() schema.GroupVersionKind { return c.gvk } // Cache returns a cache for the objects T. func (c *Controller[T, TList]) Cache() CacheInterface[T] { return NewCache[T](c.Informer().GetIndexer(), c.groupResource) } // Create creates a new object and return the newly created Object or an error. func (c *Controller[T, TList]) Create(obj T) (T, error) { result := reflect.New(c.objType).Interface().(T) return result, c.embeddedClient.Create(context.TODO(), obj.GetNamespace(), obj, result, metav1.CreateOptions{}) } // Update updates the object and return the newly updated Object or an error. func (c *Controller[T, TList]) Update(obj T) (T, error) { result := reflect.New(c.objType).Interface().(T) return result, c.embeddedClient.Update(context.TODO(), obj.GetNamespace(), obj, result, metav1.UpdateOptions{}) } // UpdateStatus updates the Status field of a the object and return the newly updated Object or an error. // Will always return an error if the object does not have a status field. func (c *Controller[T, TList]) UpdateStatus(obj T) (T, error) { result := reflect.New(c.objType).Interface().(T) return result, c.embeddedClient.UpdateStatus(context.TODO(), obj.GetNamespace(), obj, result, metav1.UpdateOptions{}) } // Delete deletes the Object in the given name and Namespace. func (c *Controller[T, TList]) Delete(namespace, name string, options *metav1.DeleteOptions) error { if options == nil { options = &metav1.DeleteOptions{} } return c.embeddedClient.Delete(context.TODO(), namespace, name, *options) } // Get gets returns the given resource with the given name in the provided namespace. func (c *Controller[T, TList]) Get(namespace, name string, options metav1.GetOptions) (T, error) { result := reflect.New(c.objType).Interface().(T) return result, c.embeddedClient.Get(context.TODO(), namespace, name, result, options) } // List will attempt to find resources in the given namespace. func (c *Controller[T, TList]) List(namespace string, opts metav1.ListOptions) (TList, error) { result := reflect.New(c.objListType).Interface().(TList) return result, c.embeddedClient.List(context.TODO(), namespace, result, opts) } // Watch will start watching resources in the given namespace. func (c *Controller[T, TList]) Watch(namespace string, opts metav1.ListOptions) (watch.Interface, error) { return c.embeddedClient.Watch(context.TODO(), namespace, opts) } // Patch will patch the resource with the matching name in the matching namespace. func (c *Controller[T, TList]) Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (T, error) { result := reflect.New(c.objType).Interface().(T) return result, c.embeddedClient.Patch(context.TODO(), namespace, name, pt, data, result, metav1.PatchOptions{}, subresources...) } // DeleteCollection will delete the resources in the given namespace matching // the listOpts. func (c *Controller[T, TList]) DeleteCollection(namespace string, deleteOpts metav1.DeleteOptions, listOpts metav1.ListOptions) error { return c.embeddedClient.DeleteCollection(context.TODO(), namespace, deleteOpts, listOpts) } // WithImpersonation returns a new copy of the client that uses impersonation. func (c *Controller[T, TList]) WithImpersonation(impersonate rest.ImpersonationConfig) (ClientInterface[T, TList], error) { newClient, err := c.embeddedClient.WithImpersonation(impersonate) if err != nil { return nil, fmt.Errorf("failed to make new client: %w", err) } // return a new controller with a new embeddedClient return &Controller[T, TList]{ controller: c.controller, embeddedClient: newClient, objType: c.objType, objListType: c.objListType, gvk: c.gvk, groupResource: c.groupResource, }, nil } // NewNonNamespacedController returns a Controller controller that is not namespaced. // NonNamespacedController redefines specific functions to no longer accept the namespace parameter. func NewNonNamespacedController[T RuntimeMetaObject, TList runtime.Object](gvk schema.GroupVersionKind, resource string, controller controller.SharedControllerFactory, ) *NonNamespacedController[T, TList] { ctrl := NewController[T, TList](gvk, resource, false, controller) return &NonNamespacedController[T, TList]{ Controller: ctrl, } } // Enqueue calls Controller.Enqueue(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) Enqueue(name string) { c.Controller.Enqueue(metav1.NamespaceAll, name) } // EnqueueAfter calls Controller.EnqueueAfter(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) EnqueueAfter(name string, duration time.Duration) { c.Controller.EnqueueAfter(metav1.NamespaceAll, name, duration) } // Delete calls Controller.Delete(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) Delete(name string, options *metav1.DeleteOptions) error { return c.Controller.Delete(metav1.NamespaceAll, name, options) } // Get calls Controller.Get(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) Get(name string, options metav1.GetOptions) (T, error) { return c.Controller.Get(metav1.NamespaceAll, name, options) } // List calls Controller.List(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) List(opts metav1.ListOptions) (TList, error) { return c.Controller.List(metav1.NamespaceAll, opts) } // Watch calls Controller.Watch(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) Watch(opts metav1.ListOptions) (watch.Interface, error) { return c.Controller.Watch(metav1.NamespaceAll, opts) } // Patch calls the Controller.Patch(...) with an empty namespace parameter. func (c *NonNamespacedController[T, TList]) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (T, error) { return c.Controller.Patch(metav1.NamespaceAll, name, pt, data, subresources...) } // WithImpersonation returns a new copy of the client that uses impersonation. func (c *NonNamespacedController[T, TList]) WithImpersonation(impersonate rest.ImpersonationConfig) (NonNamespacedClientInterface[T, TList], error) { newClient, err := c.Controller.WithImpersonation(impersonate) if err != nil { return nil, fmt.Errorf("failed to make new client: %w", err) } // get the underlying controller so we can wrap it in a NonNamespacedController newCtrl, ok := newClient.(*Controller[T, TList]) if !ok { return nil, fmt.Errorf("failed to make new client from: %T", newCtrl) } return &NonNamespacedController[T, TList]{newCtrl}, nil } // Cache calls ControllerInterface.Cache(...) and wraps the result in a new NonNamespacedCache. func (c *NonNamespacedController[T, TList]) Cache() NonNamespacedCacheInterface[T] { return NewNonNamespacedCache[T](c.Informer().GetIndexer(), c.groupResource) } // DeleteCollection will delete the resources matching the listOpts. func (c *NonNamespacedController[T, TList]) DeleteCollection(deleteOpts metav1.DeleteOptions, listOpts metav1.ListOptions) error { return c.Controller.DeleteCollection(metav1.NamespaceAll, deleteOpts, listOpts) } ================================================ FILE: pkg/generic/controllerFactoryMocks_test.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/rancher/lasso/pkg/controller (interfaces: SharedControllerFactory,SharedController) // // Generated by this command: // // mockgen --build_flags=--mod=mod -package generic -destination ./controllerFactoryMocks_test.go github.com/rancher/lasso/pkg/controller SharedControllerFactory,SharedController // // Package generic is a generated GoMock package. package generic import ( context "context" reflect "reflect" time "time" cache "github.com/rancher/lasso/pkg/cache" client "github.com/rancher/lasso/pkg/client" controller "github.com/rancher/lasso/pkg/controller" gomock "go.uber.org/mock/gomock" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache0 "k8s.io/client-go/tools/cache" ) // MockSharedControllerFactory is a mock of SharedControllerFactory interface. type MockSharedControllerFactory struct { ctrl *gomock.Controller recorder *MockSharedControllerFactoryMockRecorder isgomock struct{} } // MockSharedControllerFactoryMockRecorder is the mock recorder for MockSharedControllerFactory. type MockSharedControllerFactoryMockRecorder struct { mock *MockSharedControllerFactory } // NewMockSharedControllerFactory creates a new mock instance. func NewMockSharedControllerFactory(ctrl *gomock.Controller) *MockSharedControllerFactory { mock := &MockSharedControllerFactory{ctrl: ctrl} mock.recorder = &MockSharedControllerFactoryMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockSharedControllerFactory) EXPECT() *MockSharedControllerFactoryMockRecorder { return m.recorder } // ForKind mocks base method. func (m *MockSharedControllerFactory) ForKind(gvk schema.GroupVersionKind) (controller.SharedController, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ForKind", gvk) ret0, _ := ret[0].(controller.SharedController) ret1, _ := ret[1].(error) return ret0, ret1 } // ForKind indicates an expected call of ForKind. func (mr *MockSharedControllerFactoryMockRecorder) ForKind(gvk any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForKind", reflect.TypeOf((*MockSharedControllerFactory)(nil).ForKind), gvk) } // ForObject mocks base method. func (m *MockSharedControllerFactory) ForObject(obj runtime.Object) (controller.SharedController, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ForObject", obj) ret0, _ := ret[0].(controller.SharedController) ret1, _ := ret[1].(error) return ret0, ret1 } // ForObject indicates an expected call of ForObject. func (mr *MockSharedControllerFactoryMockRecorder) ForObject(obj any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForObject", reflect.TypeOf((*MockSharedControllerFactory)(nil).ForObject), obj) } // ForResource mocks base method. func (m *MockSharedControllerFactory) ForResource(gvr schema.GroupVersionResource, namespaced bool) controller.SharedController { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ForResource", gvr, namespaced) ret0, _ := ret[0].(controller.SharedController) return ret0 } // ForResource indicates an expected call of ForResource. func (mr *MockSharedControllerFactoryMockRecorder) ForResource(gvr, namespaced any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForResource", reflect.TypeOf((*MockSharedControllerFactory)(nil).ForResource), gvr, namespaced) } // ForResourceKind mocks base method. func (m *MockSharedControllerFactory) ForResourceKind(gvr schema.GroupVersionResource, kind string, namespaced bool) controller.SharedController { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ForResourceKind", gvr, kind, namespaced) ret0, _ := ret[0].(controller.SharedController) return ret0 } // ForResourceKind indicates an expected call of ForResourceKind. func (mr *MockSharedControllerFactoryMockRecorder) ForResourceKind(gvr, kind, namespaced any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForResourceKind", reflect.TypeOf((*MockSharedControllerFactory)(nil).ForResourceKind), gvr, kind, namespaced) } // SharedCacheFactory mocks base method. func (m *MockSharedControllerFactory) SharedCacheFactory() cache.SharedCacheFactory { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SharedCacheFactory") ret0, _ := ret[0].(cache.SharedCacheFactory) return ret0 } // SharedCacheFactory indicates an expected call of SharedCacheFactory. func (mr *MockSharedControllerFactoryMockRecorder) SharedCacheFactory() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SharedCacheFactory", reflect.TypeOf((*MockSharedControllerFactory)(nil).SharedCacheFactory)) } // Start mocks base method. func (m *MockSharedControllerFactory) Start(ctx context.Context, workers int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Start", ctx, workers) ret0, _ := ret[0].(error) return ret0 } // Start indicates an expected call of Start. func (mr *MockSharedControllerFactoryMockRecorder) Start(ctx, workers any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockSharedControllerFactory)(nil).Start), ctx, workers) } // MockSharedController is a mock of SharedController interface. type MockSharedController struct { ctrl *gomock.Controller recorder *MockSharedControllerMockRecorder isgomock struct{} } // MockSharedControllerMockRecorder is the mock recorder for MockSharedController. type MockSharedControllerMockRecorder struct { mock *MockSharedController } // NewMockSharedController creates a new mock instance. func NewMockSharedController(ctrl *gomock.Controller) *MockSharedController { mock := &MockSharedController{ctrl: ctrl} mock.recorder = &MockSharedControllerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockSharedController) EXPECT() *MockSharedControllerMockRecorder { return m.recorder } // Client mocks base method. func (m *MockSharedController) Client() *client.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Client") ret0, _ := ret[0].(*client.Client) return ret0 } // Client indicates an expected call of Client. func (mr *MockSharedControllerMockRecorder) Client() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockSharedController)(nil).Client)) } // Enqueue mocks base method. func (m *MockSharedController) Enqueue(namespace, name string) { m.ctrl.T.Helper() m.ctrl.Call(m, "Enqueue", namespace, name) } // Enqueue indicates an expected call of Enqueue. func (mr *MockSharedControllerMockRecorder) Enqueue(namespace, name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enqueue", reflect.TypeOf((*MockSharedController)(nil).Enqueue), namespace, name) } // EnqueueAfter mocks base method. func (m *MockSharedController) EnqueueAfter(namespace, name string, delay time.Duration) { m.ctrl.T.Helper() m.ctrl.Call(m, "EnqueueAfter", namespace, name, delay) } // EnqueueAfter indicates an expected call of EnqueueAfter. func (mr *MockSharedControllerMockRecorder) EnqueueAfter(namespace, name, delay any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueAfter", reflect.TypeOf((*MockSharedController)(nil).EnqueueAfter), namespace, name, delay) } // EnqueueKey mocks base method. func (m *MockSharedController) EnqueueKey(key string) { m.ctrl.T.Helper() m.ctrl.Call(m, "EnqueueKey", key) } // EnqueueKey indicates an expected call of EnqueueKey. func (mr *MockSharedControllerMockRecorder) EnqueueKey(key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueKey", reflect.TypeOf((*MockSharedController)(nil).EnqueueKey), key) } // Informer mocks base method. func (m *MockSharedController) Informer() cache0.SharedIndexInformer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Informer") ret0, _ := ret[0].(cache0.SharedIndexInformer) return ret0 } // Informer indicates an expected call of Informer. func (mr *MockSharedControllerMockRecorder) Informer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockSharedController)(nil).Informer)) } // RegisterHandler mocks base method. func (m *MockSharedController) RegisterHandler(ctx context.Context, name string, handler controller.SharedControllerHandler) { m.ctrl.T.Helper() m.ctrl.Call(m, "RegisterHandler", ctx, name, handler) } // RegisterHandler indicates an expected call of RegisterHandler. func (mr *MockSharedControllerMockRecorder) RegisterHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterHandler", reflect.TypeOf((*MockSharedController)(nil).RegisterHandler), ctx, name, handler) } // Start mocks base method. func (m *MockSharedController) Start(ctx context.Context, workers int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Start", ctx, workers) ret0, _ := ret[0].(error) return ret0 } // Start indicates an expected call of Start. func (mr *MockSharedControllerMockRecorder) Start(ctx, workers any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockSharedController)(nil).Start), ctx, workers) } ================================================ FILE: pkg/generic/controller_test.go ================================================ // Mocks for this test are generated with the following command. //go:generate sh -c "rm -f *Mocks_test.go" //go:generate mockgen --build_flags=--mod=mod -package generic -destination ./controllerFactoryMocks_test.go github.com/rancher/lasso/pkg/controller SharedControllerFactory,SharedController //go:generate mockgen -package generic -destination ./clientMocks_test.go -source ./embeddedClient.go package generic import ( "context" "fmt" "testing" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" ) const ( globalTestPodName = "High-Noon-Harry" globalTestNamespace = "rodeo" globalTestNodeName = "cowboy-server" ) var ( // Interface implementation complile time check _ ControllerInterface[*v1.Pod, *v1.PodList] = &Controller[*v1.Pod, *v1.PodList]{} _ NonNamespacedControllerInterface[*v1.Pod, *v1.PodList] = &NonNamespacedController[*v1.Pod, *v1.PodList]{} _ ClientInterface[*v1.Pod, *v1.PodList] = &Controller[*v1.Pod, *v1.PodList]{} _ NonNamespacedClientInterface[*v1.Pod, *v1.PodList] = &NonNamespacedController[*v1.Pod, *v1.PodList]{} _ CacheInterface[*v1.Pod] = &Cache[*v1.Pod]{} _ NonNamespacedCacheInterface[*v1.Pod] = &NonNamespacedCache[*v1.Pod]{} ) var errExpected = fmt.Errorf("test-error") func TestController_Get(parentT *testing.T) { parentT.Parallel() testNamespace := globalTestNamespace var testController *Controller[*v1.Pod, *v1.PodList] var testNonNamespaceController *NonNamespacedController[*v1.Pod, *v1.PodList] test := func(t *testing.T) { testOptions := metav1.GetOptions{ ResourceVersion: "3", } ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) pod := &v1.Pod{} mockClient.EXPECT().Get(context.TODO(), testNamespace, globalTestPodName, gomock.AssignableToTypeOf(pod), testOptions).DoAndReturn( func(ctx context.Context, namespace string, name string, result runtime.Object, options metav1.GetOptions) error { resultPod, ok := result.(*v1.Pod) require.True(t, ok, "Created result object was the incorrect type.") resultPod.Spec.NodeName = globalTestNodeName return nil }) var newPod *v1.Pod var err error if testNamespace == metav1.NamespaceAll { testNonNamespaceController = NewTestNonNamespacedController(ctrl, mockClient) newPod, err = testNonNamespaceController.Get(globalTestPodName, testOptions) } else { testController = NewTestController(ctrl, mockClient) newPod, err = testController.Get(testNamespace, globalTestPodName, testOptions) } require.NoError(t, err, "Error when calling get.") require.Equal(t, globalTestNodeName, newPod.Spec.NodeName, "Get call did not correctly persist pod changes from the embeddedClient.") mockClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) if testNamespace == metav1.NamespaceAll { _, err = testNonNamespaceController.Get(globalTestPodName, testOptions) } else { _, err = testController.Get(testNamespace, globalTestPodName, testOptions) } require.Error(t, err, "Error from client.Get() was not propagated") } parentT.Run("Namespaced", test) testNamespace = metav1.NamespaceAll parentT.Run("NonNamespaced", test) } func TestController_List(parentT *testing.T) { parentT.Parallel() testNamespace := globalTestNamespace var testController *Controller[*v1.Pod, *v1.PodList] var testNonNamespaceController *NonNamespacedController[*v1.Pod, *v1.PodList] test := func(t *testing.T) { testOptions := metav1.ListOptions{ ResourceVersion: "3", } ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) pod := &v1.PodList{} mockClient.EXPECT().List(context.TODO(), testNamespace, gomock.AssignableToTypeOf(pod), testOptions).DoAndReturn( func(ctx context.Context, namespace string, result runtime.Object, options metav1.ListOptions) error { pods, ok := result.(*v1.PodList) require.True(t, ok, "Created result object was the incorrect type.") pods.Items = []v1.Pod{{}} pods.Items[0].Spec.NodeName = globalTestNodeName return nil }) var newPods *v1.PodList var err error if testNamespace == metav1.NamespaceAll { testNonNamespaceController = NewTestNonNamespacedController(ctrl, mockClient) newPods, err = testNonNamespaceController.List(testOptions) } else { testController = NewTestController(ctrl, mockClient) newPods, err = testController.List(testNamespace, testOptions) } require.NoError(t, err, "Error when calling list.") require.Equal(t, globalTestNodeName, newPods.Items[0].Spec.NodeName, "List call did not correctly persist pod changes from the embeddedClient") mockClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) if testNamespace == metav1.NamespaceAll { _, err = testNonNamespaceController.List(testOptions) } else { _, err = testController.List(testNamespace, testOptions) } require.Error(t, err, "Error from client.List(...) was not propagated") } parentT.Run("Namespaced", test) testNamespace = metav1.NamespaceAll parentT.Run("NonNamespaced", test) } func TestController_Watch(parentT *testing.T) { parentT.Parallel() testNamespace := globalTestNamespace var testController *Controller[*v1.Pod, *v1.PodList] var testNonNamespaceController *NonNamespacedController[*v1.Pod, *v1.PodList] test := func(t *testing.T) { testOptions := metav1.ListOptions{ ResourceVersion: "3", } ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) emptyWatch := watch.NewEmptyWatch() mockClient.EXPECT().Watch(context.TODO(), testNamespace, testOptions).Return(emptyWatch, nil) var watchInterface watch.Interface var err error if testNamespace == metav1.NamespaceAll { testNonNamespaceController = NewTestNonNamespacedController(ctrl, mockClient) watchInterface, err = testNonNamespaceController.Watch(testOptions) } else { testController = NewTestController(ctrl, mockClient) watchInterface, err = testController.Watch(testNamespace, testOptions) } require.NoError(t, err, "Error when calling watch.") require.Equal(t, emptyWatch, watchInterface, "Watch call did not send the watch interface from the request") mockClient.EXPECT().Watch(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errExpected) if testNamespace == metav1.NamespaceAll { _, err = testNonNamespaceController.Watch(testOptions) } else { _, err = testController.Watch(testNamespace, testOptions) } require.Error(t, err, "Error from client.Watch(...) was not propagated") } parentT.Run("Namespaced", test) testNamespace = metav1.NamespaceAll parentT.Run("NonNamespaced", test) } func TestController_Patch(parentT *testing.T) { parentT.Parallel() testNamespace := globalTestNamespace var testController *Controller[*v1.Pod, *v1.PodList] var testNonNamespaceController *NonNamespacedController[*v1.Pod, *v1.PodList] test := func(t *testing.T) { testOptions := metav1.PatchOptions{} testPT := types.JSONPatchType testData := []byte(globalTestNodeName) subResources := []string{"sub", "resources"} ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) pod := &v1.Pod{} mockClient.EXPECT().Patch(context.TODO(), testNamespace, globalTestPodName, testPT, testData, gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(testOptions), subResources).DoAndReturn( func(ctx context.Context, namespace string, name string, pt types.PatchType, data []byte, result runtime.Object, opts metav1.PatchOptions, subresources ...string) error { resultPod, ok := result.(*v1.Pod) require.True(t, ok, "Created result object was the incorrect type.") resultPod.Spec.NodeName = globalTestNodeName require.Equal(t, testOptions, opts, "Patch received unexpected patch options.") return nil }) var newPod *v1.Pod var err error if testNamespace == metav1.NamespaceAll { testNonNamespaceController = NewTestNonNamespacedController(ctrl, mockClient) newPod, err = testNonNamespaceController.Patch(globalTestPodName, testPT, testData, subResources...) } else { testController = NewTestController(ctrl, mockClient) newPod, err = testController.Patch(testNamespace, globalTestPodName, testPT, testData, subResources...) } require.NoError(t, err, "Error when calling patch.") require.Equal(t, globalTestNodeName, newPod.Spec.NodeName, "Patch call did not correctly persist pod changes from the embeddedClient") mockClient.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) if testNamespace == metav1.NamespaceAll { _, err = testNonNamespaceController.Patch(globalTestPodName, testPT, testData, subResources...) } else { _, err = testController.Patch(testNamespace, globalTestPodName, testPT, testData, subResources...) } require.Error(t, err, "Error from client.Patch(...) was not propagated") } parentT.Run("Namespaced", test) testNamespace = metav1.NamespaceAll parentT.Run("NonNamespaced", test) } func TestController_Update(t *testing.T) { t.Parallel() testOptions := metav1.UpdateOptions{} ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) pod := &v1.Pod{} mockClient.EXPECT().Update(context.TODO(), globalTestNamespace, gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(testOptions)).DoAndReturn( func(ctx context.Context, namespace string, obj runtime.Object, result runtime.Object, opts metav1.UpdateOptions) error { updatePod, ok := obj.(*v1.Pod) require.True(t, ok, "Obj to update is the incorrect type.") require.Equal(t, updatePod, pod, "Incorrect obj to update was sent to the client.") resultPod, ok := result.(*v1.Pod) require.True(t, ok, "Created result object was the incorrect type.") resultPod.Spec.NodeName = globalTestNodeName require.Equal(t, testOptions, opts, "Update received unexpected update options.") return nil }) testController := NewTestController(ctrl, mockClient) pod.Namespace = globalTestNamespace newPod, err := testController.Update(pod) require.NoError(t, err, "Error when calling update.") require.Equal(t, globalTestNodeName, newPod.Spec.NodeName, "Update call did not correctly persist pod changes from the embeddedClient") mockClient.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) _, err = testController.Update(pod) require.Error(t, err, "Error from client.Update(...) was not propagated") } func TestController_UpdateStatus(t *testing.T) { t.Parallel() testOptions := metav1.UpdateOptions{} ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) pod := &v1.Pod{} mockClient.EXPECT().UpdateStatus(context.TODO(), globalTestNamespace, gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(testOptions)).DoAndReturn( func(ctx context.Context, namespace string, obj runtime.Object, result runtime.Object, opts metav1.UpdateOptions) error { updatePod, ok := obj.(*v1.Pod) require.True(t, ok, "obj to updateStatus is the incorrect type") require.Equal(t, updatePod, pod, "incorrect obj to update was sent to the client") resultPod, ok := result.(*v1.Pod) require.True(t, ok, "Created result object was the incorrect type.") resultPod.Status.Reason = globalTestNodeName require.Equal(t, testOptions, opts, "UpdateStatus received unexpected update options.") return nil }) testController := NewTestController(ctrl, mockClient) pod.Namespace = globalTestNamespace newPod, err := testController.UpdateStatus(pod) require.NoError(t, err, "Error when calling UpdateStatus(...).") require.Equal(t, globalTestNodeName, newPod.Status.Reason, "UpdateStatus call did not correctly persist pod changes from the embeddedClient") mockClient.EXPECT().UpdateStatus(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) _, err = testController.UpdateStatus(pod) require.Error(t, err, "Error from client.UpdateStatus(...) was not propagated") } func TestController_Create(t *testing.T) { t.Parallel() testOptions := metav1.CreateOptions{} ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) pod := &v1.Pod{} mockClient.EXPECT().Create(context.TODO(), globalTestNamespace, gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(pod), gomock.AssignableToTypeOf(testOptions)).DoAndReturn( func(ctx context.Context, namespace string, obj runtime.Object, result runtime.Object, opts metav1.CreateOptions) error { createPod, ok := obj.(*v1.Pod) require.True(t, ok, "obj to create is the incorrect type") require.Equal(t, createPod, pod) resultPod, ok := result.(*v1.Pod) require.True(t, ok, "Created result object was the incorrect type.") resultPod.Spec.NodeName = globalTestNodeName require.Equal(t, testOptions, opts, "Create received unexpected create options.") return nil }) testController := NewTestController(ctrl, mockClient) pod.Namespace = globalTestNamespace newPod, err := testController.Create(pod) require.NoError(t, err, "Error when calling create.") require.Equal(t, globalTestNodeName, newPod.Spec.NodeName, "Create call did not correctly persist pod changes from the embeddedClient") mockClient.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) _, err = testController.Create(pod) require.Error(t, err, "Error from client.Create(...) was not propagated") } func TestController_Delete(parentT *testing.T) { parentT.Parallel() testNamespace := globalTestNamespace var testController *Controller[*v1.Pod, *v1.PodList] var testNonNamespaceController *NonNamespacedController[*v1.Pod, *v1.PodList] test := func(t *testing.T) { testOptions := metav1.DeleteOptions{} ctrl := gomock.NewController(t) mockClient := NewMockembeddedClient(ctrl) mockClient.EXPECT().Delete(context.TODO(), testNamespace, globalTestPodName, testOptions).Return(nil) var err error if testNamespace == metav1.NamespaceAll { testNonNamespaceController = NewTestNonNamespacedController(ctrl, mockClient) err = testNonNamespaceController.Delete(globalTestPodName, &testOptions) } else { testController = NewTestController(ctrl, mockClient) err = testController.Delete(testNamespace, globalTestPodName, &testOptions) } require.NoError(t, err, "Error when calling delete.") mockClient.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) if testNamespace == metav1.NamespaceAll { err = testNonNamespaceController.Delete(globalTestPodName, &testOptions) } else { err = testController.Delete(testNamespace, globalTestPodName, &testOptions) } require.Error(t, err, "Error from client.Delete(...) was not propagated") } parentT.Run("Namespaced", test) testNamespace = metav1.NamespaceAll parentT.Run("NonNamespaced", test) } func NewTestController(ctrl *gomock.Controller, testClient embeddedClient) *Controller[*v1.Pod, *v1.PodList] { // create mock that allows the new function to run without panic mockFactory := NewMockSharedControllerFactory(ctrl) mockController := NewMockSharedController(ctrl) mockController.EXPECT().Client().Return(nil) mockFactory.EXPECT().ForResourceKind(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockController) newController := NewController[*v1.Pod, *v1.PodList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, "pods", true, mockFactory) // override the nil controller client with the test client newController.embeddedClient = testClient return newController } func NewTestNonNamespacedController(ctrl *gomock.Controller, testClient embeddedClient) *NonNamespacedController[*v1.Pod, *v1.PodList] { // create mock that allows the new function to run without panic mockFactory := NewMockSharedControllerFactory(ctrl) mockController := NewMockSharedController(ctrl) mockController.EXPECT().Client().Return(nil) mockFactory.EXPECT().ForResourceKind(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockController) newController := NewNonNamespacedController[*v1.Pod, *v1.PodList](schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, "pods", mockFactory) // override the nil controller client with the test client newController.embeddedClient = testClient return newController } ================================================ FILE: pkg/generic/embeddedClient.go ================================================ package generic import ( "context" "github.com/rancher/lasso/pkg/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/rest" ) // embeddedClient is the interface for the lasso Client used by the controller. type embeddedClient interface { Create(ctx context.Context, namespace string, obj runtime.Object, result runtime.Object, opts metav1.CreateOptions) (err error) Delete(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error Get(ctx context.Context, namespace string, name string, result runtime.Object, options metav1.GetOptions) (err error) List(ctx context.Context, namespace string, result runtime.Object, opts metav1.ListOptions) (err error) Patch(ctx context.Context, namespace string, name string, pt types.PatchType, data []byte, result runtime.Object, opts metav1.PatchOptions, subresources ...string) (err error) Update(ctx context.Context, namespace string, obj runtime.Object, result runtime.Object, opts metav1.UpdateOptions) (err error) UpdateStatus(ctx context.Context, namespace string, obj runtime.Object, result runtime.Object, opts metav1.UpdateOptions) (err error) Watch(ctx context.Context, namespace string, opts metav1.ListOptions) (watch.Interface, error) WithImpersonation(impersonate rest.ImpersonationConfig) (*client.Client, error) DeleteCollection(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error } ================================================ FILE: pkg/generic/factory.go ================================================ package generic import ( "context" "fmt" "sync" "time" "github.com/rancher/lasso/pkg/log" "github.com/sirupsen/logrus" "github.com/rancher/lasso/pkg/cache" "github.com/rancher/lasso/pkg/client" "github.com/rancher/lasso/pkg/controller" "github.com/rancher/wrangler/v3/pkg/schemes" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/rest" ) func init() { log.Infof = logrus.Infof log.Errorf = logrus.Errorf } type Factory struct { lock sync.Mutex cacheFactory cache.SharedCacheFactory controllerFactory controller.SharedControllerFactory threadiness map[schema.GroupVersionKind]int config *rest.Config opts FactoryOptions } type FactoryOptions struct { Namespace string Resync time.Duration SharedCacheFactory cache.SharedCacheFactory SharedControllerFactory controller.SharedControllerFactory HealthCallback func(bool) } func NewFactoryFromConfigWithOptions(config *rest.Config, opts *FactoryOptions) (*Factory, error) { if opts == nil { opts = &FactoryOptions{} } f := &Factory{ config: config, threadiness: map[schema.GroupVersionKind]int{}, cacheFactory: opts.SharedCacheFactory, controllerFactory: opts.SharedControllerFactory, opts: *opts, } if f.cacheFactory == nil && f.controllerFactory != nil { f.cacheFactory = f.controllerFactory.SharedCacheFactory() } return f, nil } func (c *Factory) SetThreadiness(gvk schema.GroupVersionKind, threadiness int) { c.threadiness[gvk] = threadiness } func (c *Factory) ControllerFactory() controller.SharedControllerFactory { err := c.setControllerFactoryWithLock() utilruntime.Must(err) return c.controllerFactory } func (c *Factory) setControllerFactoryWithLock() error { c.lock.Lock() defer c.lock.Unlock() if c.controllerFactory != nil { return nil } cacheFactory := c.cacheFactory if cacheFactory == nil { client, err := client.NewSharedClientFactory(c.config, &client.SharedClientFactoryOptions{ Scheme: schemes.All, }) if err != nil { return err } cacheFactory = cache.NewSharedCachedFactory(client, &cache.SharedCacheFactoryOptions{ DefaultNamespace: c.opts.Namespace, DefaultResync: c.opts.Resync, HealthCallback: c.opts.HealthCallback, }) } c.cacheFactory = cacheFactory c.controllerFactory = controller.NewSharedControllerFactory(cacheFactory, &controller.SharedControllerFactoryOptions{ KindWorkers: c.threadiness, }) return nil } func (c *Factory) Sync(ctx context.Context) error { if c.cacheFactory != nil { if err := c.cacheFactory.Start(ctx); err != nil { return err } syncStatus := c.cacheFactory.WaitForCacheSync(ctx) for informerType, synced := range syncStatus { if !synced { return fmt.Errorf("cache sync failed for informer %s", informerType.Kind) } } } return nil } func (c *Factory) Start(ctx context.Context, defaultThreadiness int) error { if err := c.Sync(ctx); err != nil { return err } if c.controllerFactory != nil { return c.controllerFactory.Start(ctx, defaultThreadiness) } return nil } ================================================ FILE: pkg/generic/fake/README.md ================================================ # Generic Controller, Client, and Cache Mocks This package leverages https://github.com/golang/mock for using generic mocks in tests. [gomock](https://pkg.go.dev/github.com/golang/mock/gomock) holds more specific information on different ways to use gomock in test. ## Usage This package has four entry points for creating a mock controller, client, or cache interface.
Note: A mock controller will implement both a generic.ControllerInterface and a generic.ClientInterface. - `NewMockControllerInterface[T runtime.Object, TList runtime.Object](*gomock.Controller)` - `NewMockNonNamespacedControllerInterface[T runtime.Object, TList runtime.Object](*gomock.Controller)` - `NewCacheInterface[T runtime.Object](*gomock.Controller)` - `NewNonNamespaceCacheInterface[T runtime.Object](*gomock.Controller)` ## Examples Example use of generic/fake with a generated Deployment Controller. ``` golang // Generated controller interface to mock. type DeploymentController interface { generic.ControllerInterface[*v1.Deployment, *v1.DeploymentList] } ``` ``` golang // Example Test Function import ( "testing" "github.com/golang/mock/gomock" wranglerv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1" "github.com/rancher/wrangler/v3/pkg/generic/fake" v1 "k8s.io/api/apps/v1" ) func TestController(t *testing.T){ // Create gomock controller. This is used by the gomock library. gomockCtrl := gomock.NewController(t) // Create a new Generic Controller Mock with type apps1.Deployment. deployMock := fake.NewMockControllerInterface[*v1.Deployment, *v1.DeploymentList](ctrl) // Define expected calls to our mock controller using gomock. deployMock.EXPECT().Enqueue("test-namespace", "test-name").AnyTimes() // Start Test Code. // . // . // . // Test calls Enqueue with expected parameters nothing happens. deployMock.Enqueue("test-namespace", "test-name") // Test calls Enqueue with unexpected parameters. // gomock will fail the test because it did not expect the call. deployMock.Enqueue("unexpected-namespace", "unexpected-name") } ``` ### NonNamespacedController ```golang ctrl := gomock.NewController(t) mock := fake.NewMockNonNamespacedControllerInterface[*v3.RoleTemplate, *v3.RoleTemplateList](ctrl) mock.EXPECT().List(gomock.Any()).Return(nil, nil) ``` ## Fake Generation This package was generated with `mockgen` (see [`generate.go`](./generate.go)), just run: ```shell go generate ./... ``` to recreate them. #### Caveats Due to incomplete support for generics, some modifications over the original file need to be applied for the generation to succeeed. #### `controller.go` 1. Comment out the `comparable` in RuntimeMetaObject ================================================ FILE: pkg/generic/fake/cache.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: ../cache.go // // Generated by this command: // // mockgen -package fake -destination ./cache.go -source ../cache.go // // Package fake is a generated GoMock package. package fake import ( reflect "reflect" generic "github.com/rancher/wrangler/v3/pkg/generic" gomock "go.uber.org/mock/gomock" labels "k8s.io/apimachinery/pkg/labels" runtime "k8s.io/apimachinery/pkg/runtime" ) // MockCacheInterface is a mock of CacheInterface interface. type MockCacheInterface[T runtime.Object] struct { ctrl *gomock.Controller recorder *MockCacheInterfaceMockRecorder[T] isgomock struct{} } // MockCacheInterfaceMockRecorder is the mock recorder for MockCacheInterface. type MockCacheInterfaceMockRecorder[T runtime.Object] struct { mock *MockCacheInterface[T] } // NewMockCacheInterface creates a new mock instance. func NewMockCacheInterface[T runtime.Object](ctrl *gomock.Controller) *MockCacheInterface[T] { mock := &MockCacheInterface[T]{ctrl: ctrl} mock.recorder = &MockCacheInterfaceMockRecorder[T]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockCacheInterface[T]) EXPECT() *MockCacheInterfaceMockRecorder[T] { return m.recorder } // AddIndexer mocks base method. func (m *MockCacheInterface[T]) AddIndexer(indexName string, indexer generic.Indexer[T]) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddIndexer", indexName, indexer) } // AddIndexer indicates an expected call of AddIndexer. func (mr *MockCacheInterfaceMockRecorder[T]) AddIndexer(indexName, indexer any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexer", reflect.TypeOf((*MockCacheInterface[T])(nil).AddIndexer), indexName, indexer) } // Get mocks base method. func (m *MockCacheInterface[T]) Get(namespace, name string) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", namespace, name) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockCacheInterfaceMockRecorder[T]) Get(namespace, name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCacheInterface[T])(nil).Get), namespace, name) } // GetByIndex mocks base method. func (m *MockCacheInterface[T]) GetByIndex(indexName, key string) ([]T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetByIndex", indexName, key) ret0, _ := ret[0].([]T) ret1, _ := ret[1].(error) return ret0, ret1 } // GetByIndex indicates an expected call of GetByIndex. func (mr *MockCacheInterfaceMockRecorder[T]) GetByIndex(indexName, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIndex", reflect.TypeOf((*MockCacheInterface[T])(nil).GetByIndex), indexName, key) } // List mocks base method. func (m *MockCacheInterface[T]) List(namespace string, selector labels.Selector) ([]T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", namespace, selector) ret0, _ := ret[0].([]T) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockCacheInterfaceMockRecorder[T]) List(namespace, selector any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCacheInterface[T])(nil).List), namespace, selector) } // MockNonNamespacedCacheInterface is a mock of NonNamespacedCacheInterface interface. type MockNonNamespacedCacheInterface[T runtime.Object] struct { ctrl *gomock.Controller recorder *MockNonNamespacedCacheInterfaceMockRecorder[T] isgomock struct{} } // MockNonNamespacedCacheInterfaceMockRecorder is the mock recorder for MockNonNamespacedCacheInterface. type MockNonNamespacedCacheInterfaceMockRecorder[T runtime.Object] struct { mock *MockNonNamespacedCacheInterface[T] } // NewMockNonNamespacedCacheInterface creates a new mock instance. func NewMockNonNamespacedCacheInterface[T runtime.Object](ctrl *gomock.Controller) *MockNonNamespacedCacheInterface[T] { mock := &MockNonNamespacedCacheInterface[T]{ctrl: ctrl} mock.recorder = &MockNonNamespacedCacheInterfaceMockRecorder[T]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockNonNamespacedCacheInterface[T]) EXPECT() *MockNonNamespacedCacheInterfaceMockRecorder[T] { return m.recorder } // AddIndexer mocks base method. func (m *MockNonNamespacedCacheInterface[T]) AddIndexer(indexName string, indexer generic.Indexer[T]) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddIndexer", indexName, indexer) } // AddIndexer indicates an expected call of AddIndexer. func (mr *MockNonNamespacedCacheInterfaceMockRecorder[T]) AddIndexer(indexName, indexer any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexer", reflect.TypeOf((*MockNonNamespacedCacheInterface[T])(nil).AddIndexer), indexName, indexer) } // Get mocks base method. func (m *MockNonNamespacedCacheInterface[T]) Get(name string) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", name) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockNonNamespacedCacheInterfaceMockRecorder[T]) Get(name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockNonNamespacedCacheInterface[T])(nil).Get), name) } // GetByIndex mocks base method. func (m *MockNonNamespacedCacheInterface[T]) GetByIndex(indexName, key string) ([]T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetByIndex", indexName, key) ret0, _ := ret[0].([]T) ret1, _ := ret[1].(error) return ret0, ret1 } // GetByIndex indicates an expected call of GetByIndex. func (mr *MockNonNamespacedCacheInterfaceMockRecorder[T]) GetByIndex(indexName, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIndex", reflect.TypeOf((*MockNonNamespacedCacheInterface[T])(nil).GetByIndex), indexName, key) } // List mocks base method. func (m *MockNonNamespacedCacheInterface[T]) List(selector labels.Selector) ([]T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", selector) ret0, _ := ret[0].([]T) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockNonNamespacedCacheInterfaceMockRecorder[T]) List(selector any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNonNamespacedCacheInterface[T])(nil).List), selector) } ================================================ FILE: pkg/generic/fake/controller.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: ../controller.go // // Generated by this command: // // mockgen -package fake -destination ./controller.go -source ../controller.go // // Package fake is a generated GoMock package. package fake import ( context "context" reflect "reflect" time "time" generic "github.com/rancher/wrangler/v3/pkg/generic" gomock "go.uber.org/mock/gomock" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" rest "k8s.io/client-go/rest" cache "k8s.io/client-go/tools/cache" ) // MockControllerMeta is a mock of ControllerMeta interface. type MockControllerMeta struct { ctrl *gomock.Controller recorder *MockControllerMetaMockRecorder isgomock struct{} } // MockControllerMetaMockRecorder is the mock recorder for MockControllerMeta. type MockControllerMetaMockRecorder struct { mock *MockControllerMeta } // NewMockControllerMeta creates a new mock instance. func NewMockControllerMeta(ctrl *gomock.Controller) *MockControllerMeta { mock := &MockControllerMeta{ctrl: ctrl} mock.recorder = &MockControllerMetaMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockControllerMeta) EXPECT() *MockControllerMetaMockRecorder { return m.recorder } // AddGenericHandler mocks base method. func (m *MockControllerMeta) AddGenericHandler(ctx context.Context, name string, handler generic.Handler) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddGenericHandler", ctx, name, handler) } // AddGenericHandler indicates an expected call of AddGenericHandler. func (mr *MockControllerMetaMockRecorder) AddGenericHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericHandler", reflect.TypeOf((*MockControllerMeta)(nil).AddGenericHandler), ctx, name, handler) } // AddGenericRemoveHandler mocks base method. func (m *MockControllerMeta) AddGenericRemoveHandler(ctx context.Context, name string, handler generic.Handler) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddGenericRemoveHandler", ctx, name, handler) } // AddGenericRemoveHandler indicates an expected call of AddGenericRemoveHandler. func (mr *MockControllerMetaMockRecorder) AddGenericRemoveHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericRemoveHandler", reflect.TypeOf((*MockControllerMeta)(nil).AddGenericRemoveHandler), ctx, name, handler) } // GroupVersionKind mocks base method. func (m *MockControllerMeta) GroupVersionKind() schema.GroupVersionKind { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GroupVersionKind") ret0, _ := ret[0].(schema.GroupVersionKind) return ret0 } // GroupVersionKind indicates an expected call of GroupVersionKind. func (mr *MockControllerMetaMockRecorder) GroupVersionKind() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKind", reflect.TypeOf((*MockControllerMeta)(nil).GroupVersionKind)) } // Informer mocks base method. func (m *MockControllerMeta) Informer() cache.SharedIndexInformer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Informer") ret0, _ := ret[0].(cache.SharedIndexInformer) return ret0 } // Informer indicates an expected call of Informer. func (mr *MockControllerMetaMockRecorder) Informer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockControllerMeta)(nil).Informer)) } // Updater mocks base method. func (m *MockControllerMeta) Updater() generic.Updater { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Updater") ret0, _ := ret[0].(generic.Updater) return ret0 } // Updater indicates an expected call of Updater. func (mr *MockControllerMetaMockRecorder) Updater() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updater", reflect.TypeOf((*MockControllerMeta)(nil).Updater)) } // MockRuntimeMetaObject is a mock of RuntimeMetaObject interface. type MockRuntimeMetaObject struct { ctrl *gomock.Controller recorder *MockRuntimeMetaObjectMockRecorder isgomock struct{} } // MockRuntimeMetaObjectMockRecorder is the mock recorder for MockRuntimeMetaObject. type MockRuntimeMetaObjectMockRecorder struct { mock *MockRuntimeMetaObject } // NewMockRuntimeMetaObject creates a new mock instance. func NewMockRuntimeMetaObject(ctrl *gomock.Controller) *MockRuntimeMetaObject { mock := &MockRuntimeMetaObject{ctrl: ctrl} mock.recorder = &MockRuntimeMetaObjectMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockRuntimeMetaObject) EXPECT() *MockRuntimeMetaObjectMockRecorder { return m.recorder } // DeepCopyObject mocks base method. func (m *MockRuntimeMetaObject) DeepCopyObject() runtime.Object { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeepCopyObject") ret0, _ := ret[0].(runtime.Object) return ret0 } // DeepCopyObject indicates an expected call of DeepCopyObject. func (mr *MockRuntimeMetaObjectMockRecorder) DeepCopyObject() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeepCopyObject", reflect.TypeOf((*MockRuntimeMetaObject)(nil).DeepCopyObject)) } // GetAnnotations mocks base method. func (m *MockRuntimeMetaObject) GetAnnotations() map[string]string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAnnotations") ret0, _ := ret[0].(map[string]string) return ret0 } // GetAnnotations indicates an expected call of GetAnnotations. func (mr *MockRuntimeMetaObjectMockRecorder) GetAnnotations() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnnotations", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetAnnotations)) } // GetCreationTimestamp mocks base method. func (m *MockRuntimeMetaObject) GetCreationTimestamp() v1.Time { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCreationTimestamp") ret0, _ := ret[0].(v1.Time) return ret0 } // GetCreationTimestamp indicates an expected call of GetCreationTimestamp. func (mr *MockRuntimeMetaObjectMockRecorder) GetCreationTimestamp() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCreationTimestamp", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetCreationTimestamp)) } // GetDeletionGracePeriodSeconds mocks base method. func (m *MockRuntimeMetaObject) GetDeletionGracePeriodSeconds() *int64 { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDeletionGracePeriodSeconds") ret0, _ := ret[0].(*int64) return ret0 } // GetDeletionGracePeriodSeconds indicates an expected call of GetDeletionGracePeriodSeconds. func (mr *MockRuntimeMetaObjectMockRecorder) GetDeletionGracePeriodSeconds() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeletionGracePeriodSeconds", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetDeletionGracePeriodSeconds)) } // GetDeletionTimestamp mocks base method. func (m *MockRuntimeMetaObject) GetDeletionTimestamp() *v1.Time { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDeletionTimestamp") ret0, _ := ret[0].(*v1.Time) return ret0 } // GetDeletionTimestamp indicates an expected call of GetDeletionTimestamp. func (mr *MockRuntimeMetaObjectMockRecorder) GetDeletionTimestamp() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeletionTimestamp", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetDeletionTimestamp)) } // GetFinalizers mocks base method. func (m *MockRuntimeMetaObject) GetFinalizers() []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFinalizers") ret0, _ := ret[0].([]string) return ret0 } // GetFinalizers indicates an expected call of GetFinalizers. func (mr *MockRuntimeMetaObjectMockRecorder) GetFinalizers() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFinalizers", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetFinalizers)) } // GetGenerateName mocks base method. func (m *MockRuntimeMetaObject) GetGenerateName() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetGenerateName") ret0, _ := ret[0].(string) return ret0 } // GetGenerateName indicates an expected call of GetGenerateName. func (mr *MockRuntimeMetaObjectMockRecorder) GetGenerateName() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGenerateName", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetGenerateName)) } // GetGeneration mocks base method. func (m *MockRuntimeMetaObject) GetGeneration() int64 { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetGeneration") ret0, _ := ret[0].(int64) return ret0 } // GetGeneration indicates an expected call of GetGeneration. func (mr *MockRuntimeMetaObjectMockRecorder) GetGeneration() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGeneration", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetGeneration)) } // GetLabels mocks base method. func (m *MockRuntimeMetaObject) GetLabels() map[string]string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLabels") ret0, _ := ret[0].(map[string]string) return ret0 } // GetLabels indicates an expected call of GetLabels. func (mr *MockRuntimeMetaObjectMockRecorder) GetLabels() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLabels", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetLabels)) } // GetManagedFields mocks base method. func (m *MockRuntimeMetaObject) GetManagedFields() []v1.ManagedFieldsEntry { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetManagedFields") ret0, _ := ret[0].([]v1.ManagedFieldsEntry) return ret0 } // GetManagedFields indicates an expected call of GetManagedFields. func (mr *MockRuntimeMetaObjectMockRecorder) GetManagedFields() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetManagedFields", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetManagedFields)) } // GetName mocks base method. func (m *MockRuntimeMetaObject) GetName() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetName") ret0, _ := ret[0].(string) return ret0 } // GetName indicates an expected call of GetName. func (mr *MockRuntimeMetaObjectMockRecorder) GetName() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetName)) } // GetNamespace mocks base method. func (m *MockRuntimeMetaObject) GetNamespace() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetNamespace") ret0, _ := ret[0].(string) return ret0 } // GetNamespace indicates an expected call of GetNamespace. func (mr *MockRuntimeMetaObjectMockRecorder) GetNamespace() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespace", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetNamespace)) } // GetObjectKind mocks base method. func (m *MockRuntimeMetaObject) GetObjectKind() schema.ObjectKind { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetObjectKind") ret0, _ := ret[0].(schema.ObjectKind) return ret0 } // GetObjectKind indicates an expected call of GetObjectKind. func (mr *MockRuntimeMetaObjectMockRecorder) GetObjectKind() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectKind", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetObjectKind)) } // GetOwnerReferences mocks base method. func (m *MockRuntimeMetaObject) GetOwnerReferences() []v1.OwnerReference { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetOwnerReferences") ret0, _ := ret[0].([]v1.OwnerReference) return ret0 } // GetOwnerReferences indicates an expected call of GetOwnerReferences. func (mr *MockRuntimeMetaObjectMockRecorder) GetOwnerReferences() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOwnerReferences", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetOwnerReferences)) } // GetResourceVersion mocks base method. func (m *MockRuntimeMetaObject) GetResourceVersion() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetResourceVersion") ret0, _ := ret[0].(string) return ret0 } // GetResourceVersion indicates an expected call of GetResourceVersion. func (mr *MockRuntimeMetaObjectMockRecorder) GetResourceVersion() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceVersion", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetResourceVersion)) } // GetSelfLink mocks base method. func (m *MockRuntimeMetaObject) GetSelfLink() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSelfLink") ret0, _ := ret[0].(string) return ret0 } // GetSelfLink indicates an expected call of GetSelfLink. func (mr *MockRuntimeMetaObjectMockRecorder) GetSelfLink() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSelfLink", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetSelfLink)) } // GetUID mocks base method. func (m *MockRuntimeMetaObject) GetUID() types.UID { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetUID") ret0, _ := ret[0].(types.UID) return ret0 } // GetUID indicates an expected call of GetUID. func (mr *MockRuntimeMetaObjectMockRecorder) GetUID() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUID", reflect.TypeOf((*MockRuntimeMetaObject)(nil).GetUID)) } // SetAnnotations mocks base method. func (m *MockRuntimeMetaObject) SetAnnotations(annotations map[string]string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetAnnotations", annotations) } // SetAnnotations indicates an expected call of SetAnnotations. func (mr *MockRuntimeMetaObjectMockRecorder) SetAnnotations(annotations any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAnnotations", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetAnnotations), annotations) } // SetCreationTimestamp mocks base method. func (m *MockRuntimeMetaObject) SetCreationTimestamp(timestamp v1.Time) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetCreationTimestamp", timestamp) } // SetCreationTimestamp indicates an expected call of SetCreationTimestamp. func (mr *MockRuntimeMetaObjectMockRecorder) SetCreationTimestamp(timestamp any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCreationTimestamp", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetCreationTimestamp), timestamp) } // SetDeletionGracePeriodSeconds mocks base method. func (m *MockRuntimeMetaObject) SetDeletionGracePeriodSeconds(arg0 *int64) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetDeletionGracePeriodSeconds", arg0) } // SetDeletionGracePeriodSeconds indicates an expected call of SetDeletionGracePeriodSeconds. func (mr *MockRuntimeMetaObjectMockRecorder) SetDeletionGracePeriodSeconds(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeletionGracePeriodSeconds", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetDeletionGracePeriodSeconds), arg0) } // SetDeletionTimestamp mocks base method. func (m *MockRuntimeMetaObject) SetDeletionTimestamp(timestamp *v1.Time) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetDeletionTimestamp", timestamp) } // SetDeletionTimestamp indicates an expected call of SetDeletionTimestamp. func (mr *MockRuntimeMetaObjectMockRecorder) SetDeletionTimestamp(timestamp any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeletionTimestamp", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetDeletionTimestamp), timestamp) } // SetFinalizers mocks base method. func (m *MockRuntimeMetaObject) SetFinalizers(finalizers []string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetFinalizers", finalizers) } // SetFinalizers indicates an expected call of SetFinalizers. func (mr *MockRuntimeMetaObjectMockRecorder) SetFinalizers(finalizers any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFinalizers", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetFinalizers), finalizers) } // SetGenerateName mocks base method. func (m *MockRuntimeMetaObject) SetGenerateName(name string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetGenerateName", name) } // SetGenerateName indicates an expected call of SetGenerateName. func (mr *MockRuntimeMetaObjectMockRecorder) SetGenerateName(name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGenerateName", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetGenerateName), name) } // SetGeneration mocks base method. func (m *MockRuntimeMetaObject) SetGeneration(generation int64) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetGeneration", generation) } // SetGeneration indicates an expected call of SetGeneration. func (mr *MockRuntimeMetaObjectMockRecorder) SetGeneration(generation any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGeneration", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetGeneration), generation) } // SetLabels mocks base method. func (m *MockRuntimeMetaObject) SetLabels(labels map[string]string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetLabels", labels) } // SetLabels indicates an expected call of SetLabels. func (mr *MockRuntimeMetaObjectMockRecorder) SetLabels(labels any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLabels", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetLabels), labels) } // SetManagedFields mocks base method. func (m *MockRuntimeMetaObject) SetManagedFields(managedFields []v1.ManagedFieldsEntry) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetManagedFields", managedFields) } // SetManagedFields indicates an expected call of SetManagedFields. func (mr *MockRuntimeMetaObjectMockRecorder) SetManagedFields(managedFields any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetManagedFields", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetManagedFields), managedFields) } // SetName mocks base method. func (m *MockRuntimeMetaObject) SetName(name string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetName", name) } // SetName indicates an expected call of SetName. func (mr *MockRuntimeMetaObjectMockRecorder) SetName(name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetName", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetName), name) } // SetNamespace mocks base method. func (m *MockRuntimeMetaObject) SetNamespace(namespace string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetNamespace", namespace) } // SetNamespace indicates an expected call of SetNamespace. func (mr *MockRuntimeMetaObjectMockRecorder) SetNamespace(namespace any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamespace", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetNamespace), namespace) } // SetOwnerReferences mocks base method. func (m *MockRuntimeMetaObject) SetOwnerReferences(arg0 []v1.OwnerReference) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetOwnerReferences", arg0) } // SetOwnerReferences indicates an expected call of SetOwnerReferences. func (mr *MockRuntimeMetaObjectMockRecorder) SetOwnerReferences(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOwnerReferences", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetOwnerReferences), arg0) } // SetResourceVersion mocks base method. func (m *MockRuntimeMetaObject) SetResourceVersion(version string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetResourceVersion", version) } // SetResourceVersion indicates an expected call of SetResourceVersion. func (mr *MockRuntimeMetaObjectMockRecorder) SetResourceVersion(version any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetResourceVersion", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetResourceVersion), version) } // SetSelfLink mocks base method. func (m *MockRuntimeMetaObject) SetSelfLink(selfLink string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetSelfLink", selfLink) } // SetSelfLink indicates an expected call of SetSelfLink. func (mr *MockRuntimeMetaObjectMockRecorder) SetSelfLink(selfLink any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSelfLink", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetSelfLink), selfLink) } // SetUID mocks base method. func (m *MockRuntimeMetaObject) SetUID(uid types.UID) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetUID", uid) } // SetUID indicates an expected call of SetUID. func (mr *MockRuntimeMetaObjectMockRecorder) SetUID(uid any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUID", reflect.TypeOf((*MockRuntimeMetaObject)(nil).SetUID), uid) } // MockControllerInterface is a mock of ControllerInterface interface. type MockControllerInterface[T generic.RuntimeMetaObject, TList runtime.Object] struct { ctrl *gomock.Controller recorder *MockControllerInterfaceMockRecorder[T, TList] isgomock struct{} } // MockControllerInterfaceMockRecorder is the mock recorder for MockControllerInterface. type MockControllerInterfaceMockRecorder[T generic.RuntimeMetaObject, TList runtime.Object] struct { mock *MockControllerInterface[T, TList] } // NewMockControllerInterface creates a new mock instance. func NewMockControllerInterface[T generic.RuntimeMetaObject, TList runtime.Object](ctrl *gomock.Controller) *MockControllerInterface[T, TList] { mock := &MockControllerInterface[T, TList]{ctrl: ctrl} mock.recorder = &MockControllerInterfaceMockRecorder[T, TList]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockControllerInterface[T, TList]) EXPECT() *MockControllerInterfaceMockRecorder[T, TList] { return m.recorder } // AddGenericHandler mocks base method. func (m *MockControllerInterface[T, TList]) AddGenericHandler(ctx context.Context, name string, handler generic.Handler) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddGenericHandler", ctx, name, handler) } // AddGenericHandler indicates an expected call of AddGenericHandler. func (mr *MockControllerInterfaceMockRecorder[T, TList]) AddGenericHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericHandler", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).AddGenericHandler), ctx, name, handler) } // AddGenericRemoveHandler mocks base method. func (m *MockControllerInterface[T, TList]) AddGenericRemoveHandler(ctx context.Context, name string, handler generic.Handler) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddGenericRemoveHandler", ctx, name, handler) } // AddGenericRemoveHandler indicates an expected call of AddGenericRemoveHandler. func (mr *MockControllerInterfaceMockRecorder[T, TList]) AddGenericRemoveHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericRemoveHandler", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).AddGenericRemoveHandler), ctx, name, handler) } // Cache mocks base method. func (m *MockControllerInterface[T, TList]) Cache() generic.CacheInterface[T] { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Cache") ret0, _ := ret[0].(generic.CacheInterface[T]) return ret0 } // Cache indicates an expected call of Cache. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Cache() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cache", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Cache)) } // Create mocks base method. func (m *MockControllerInterface[T, TList]) Create(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Create(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Create), arg0) } // Delete mocks base method. func (m *MockControllerInterface[T, TList]) Delete(namespace, name string, options *v1.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", namespace, name, options) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Delete(namespace, name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Delete), namespace, name, options) } // DeleteCollection mocks base method. func (m *MockControllerInterface[T, TList]) DeleteCollection(namespace string, deleteOpts v1.DeleteOptions, listOpts v1.ListOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteCollection", namespace, deleteOpts, listOpts) ret0, _ := ret[0].(error) return ret0 } // DeleteCollection indicates an expected call of DeleteCollection. func (mr *MockControllerInterfaceMockRecorder[T, TList]) DeleteCollection(namespace, deleteOpts, listOpts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).DeleteCollection), namespace, deleteOpts, listOpts) } // Enqueue mocks base method. func (m *MockControllerInterface[T, TList]) Enqueue(namespace, name string) { m.ctrl.T.Helper() m.ctrl.Call(m, "Enqueue", namespace, name) } // Enqueue indicates an expected call of Enqueue. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Enqueue(namespace, name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enqueue", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Enqueue), namespace, name) } // EnqueueAfter mocks base method. func (m *MockControllerInterface[T, TList]) EnqueueAfter(namespace, name string, duration time.Duration) { m.ctrl.T.Helper() m.ctrl.Call(m, "EnqueueAfter", namespace, name, duration) } // EnqueueAfter indicates an expected call of EnqueueAfter. func (mr *MockControllerInterfaceMockRecorder[T, TList]) EnqueueAfter(namespace, name, duration any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueAfter", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).EnqueueAfter), namespace, name, duration) } // Get mocks base method. func (m *MockControllerInterface[T, TList]) Get(namespace, name string, options v1.GetOptions) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", namespace, name, options) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Get(namespace, name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Get), namespace, name, options) } // GroupVersionKind mocks base method. func (m *MockControllerInterface[T, TList]) GroupVersionKind() schema.GroupVersionKind { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GroupVersionKind") ret0, _ := ret[0].(schema.GroupVersionKind) return ret0 } // GroupVersionKind indicates an expected call of GroupVersionKind. func (mr *MockControllerInterfaceMockRecorder[T, TList]) GroupVersionKind() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKind", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).GroupVersionKind)) } // Informer mocks base method. func (m *MockControllerInterface[T, TList]) Informer() cache.SharedIndexInformer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Informer") ret0, _ := ret[0].(cache.SharedIndexInformer) return ret0 } // Informer indicates an expected call of Informer. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Informer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Informer)) } // List mocks base method. func (m *MockControllerInterface[T, TList]) List(namespace string, opts v1.ListOptions) (TList, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", namespace, opts) ret0, _ := ret[0].(TList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockControllerInterfaceMockRecorder[T, TList]) List(namespace, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).List), namespace, opts) } // OnChange mocks base method. func (m *MockControllerInterface[T, TList]) OnChange(ctx context.Context, name string, sync generic.ObjectHandler[T]) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnChange", ctx, name, sync) } // OnChange indicates an expected call of OnChange. func (mr *MockControllerInterfaceMockRecorder[T, TList]) OnChange(ctx, name, sync any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnChange", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).OnChange), ctx, name, sync) } // OnRemove mocks base method. func (m *MockControllerInterface[T, TList]) OnRemove(ctx context.Context, name string, sync generic.ObjectHandler[T]) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnRemove", ctx, name, sync) } // OnRemove indicates an expected call of OnRemove. func (mr *MockControllerInterfaceMockRecorder[T, TList]) OnRemove(ctx, name, sync any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnRemove", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).OnRemove), ctx, name, sync) } // Patch mocks base method. func (m *MockControllerInterface[T, TList]) Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (T, error) { m.ctrl.T.Helper() varargs := []any{namespace, name, pt, data} for _, a := range subresources { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Patch", varargs...) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Patch indicates an expected call of Patch. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Patch(namespace, name, pt, data any, subresources ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{namespace, name, pt, data}, subresources...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Patch), varargs...) } // Update mocks base method. func (m *MockControllerInterface[T, TList]) Update(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Update(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Update), arg0) } // UpdateStatus mocks base method. func (m *MockControllerInterface[T, TList]) UpdateStatus(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStatus", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateStatus indicates an expected call of UpdateStatus. func (mr *MockControllerInterfaceMockRecorder[T, TList]) UpdateStatus(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).UpdateStatus), arg0) } // Updater mocks base method. func (m *MockControllerInterface[T, TList]) Updater() generic.Updater { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Updater") ret0, _ := ret[0].(generic.Updater) return ret0 } // Updater indicates an expected call of Updater. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Updater() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updater", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Updater)) } // Watch mocks base method. func (m *MockControllerInterface[T, TList]) Watch(namespace string, opts v1.ListOptions) (watch.Interface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Watch", namespace, opts) ret0, _ := ret[0].(watch.Interface) ret1, _ := ret[1].(error) return ret0, ret1 } // Watch indicates an expected call of Watch. func (mr *MockControllerInterfaceMockRecorder[T, TList]) Watch(namespace, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).Watch), namespace, opts) } // WithImpersonation mocks base method. func (m *MockControllerInterface[T, TList]) WithImpersonation(impersonate rest.ImpersonationConfig) (generic.ClientInterface[T, TList], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithImpersonation", impersonate) ret0, _ := ret[0].(generic.ClientInterface[T, TList]) ret1, _ := ret[1].(error) return ret0, ret1 } // WithImpersonation indicates an expected call of WithImpersonation. func (mr *MockControllerInterfaceMockRecorder[T, TList]) WithImpersonation(impersonate any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithImpersonation", reflect.TypeOf((*MockControllerInterface[T, TList])(nil).WithImpersonation), impersonate) } // MockNonNamespacedControllerInterface is a mock of NonNamespacedControllerInterface interface. type MockNonNamespacedControllerInterface[T generic.RuntimeMetaObject, TList runtime.Object] struct { ctrl *gomock.Controller recorder *MockNonNamespacedControllerInterfaceMockRecorder[T, TList] isgomock struct{} } // MockNonNamespacedControllerInterfaceMockRecorder is the mock recorder for MockNonNamespacedControllerInterface. type MockNonNamespacedControllerInterfaceMockRecorder[T generic.RuntimeMetaObject, TList runtime.Object] struct { mock *MockNonNamespacedControllerInterface[T, TList] } // NewMockNonNamespacedControllerInterface creates a new mock instance. func NewMockNonNamespacedControllerInterface[T generic.RuntimeMetaObject, TList runtime.Object](ctrl *gomock.Controller) *MockNonNamespacedControllerInterface[T, TList] { mock := &MockNonNamespacedControllerInterface[T, TList]{ctrl: ctrl} mock.recorder = &MockNonNamespacedControllerInterfaceMockRecorder[T, TList]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockNonNamespacedControllerInterface[T, TList]) EXPECT() *MockNonNamespacedControllerInterfaceMockRecorder[T, TList] { return m.recorder } // AddGenericHandler mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) AddGenericHandler(ctx context.Context, name string, handler generic.Handler) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddGenericHandler", ctx, name, handler) } // AddGenericHandler indicates an expected call of AddGenericHandler. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) AddGenericHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericHandler", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).AddGenericHandler), ctx, name, handler) } // AddGenericRemoveHandler mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) AddGenericRemoveHandler(ctx context.Context, name string, handler generic.Handler) { m.ctrl.T.Helper() m.ctrl.Call(m, "AddGenericRemoveHandler", ctx, name, handler) } // AddGenericRemoveHandler indicates an expected call of AddGenericRemoveHandler. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) AddGenericRemoveHandler(ctx, name, handler any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericRemoveHandler", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).AddGenericRemoveHandler), ctx, name, handler) } // Cache mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Cache() generic.NonNamespacedCacheInterface[T] { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Cache") ret0, _ := ret[0].(generic.NonNamespacedCacheInterface[T]) return ret0 } // Cache indicates an expected call of Cache. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Cache() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cache", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Cache)) } // Create mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Create(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Create(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Create), arg0) } // Delete mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Delete(name string, options *v1.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", name, options) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Delete(name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Delete), name, options) } // Enqueue mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Enqueue(name string) { m.ctrl.T.Helper() m.ctrl.Call(m, "Enqueue", name) } // Enqueue indicates an expected call of Enqueue. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Enqueue(name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enqueue", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Enqueue), name) } // EnqueueAfter mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) EnqueueAfter(name string, duration time.Duration) { m.ctrl.T.Helper() m.ctrl.Call(m, "EnqueueAfter", name, duration) } // EnqueueAfter indicates an expected call of EnqueueAfter. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) EnqueueAfter(name, duration any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueAfter", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).EnqueueAfter), name, duration) } // Get mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Get(name string, options v1.GetOptions) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", name, options) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Get(name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Get), name, options) } // GroupVersionKind mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) GroupVersionKind() schema.GroupVersionKind { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GroupVersionKind") ret0, _ := ret[0].(schema.GroupVersionKind) return ret0 } // GroupVersionKind indicates an expected call of GroupVersionKind. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) GroupVersionKind() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKind", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).GroupVersionKind)) } // Informer mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Informer() cache.SharedIndexInformer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Informer") ret0, _ := ret[0].(cache.SharedIndexInformer) return ret0 } // Informer indicates an expected call of Informer. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Informer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Informer)) } // List mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) List(opts v1.ListOptions) (TList, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", opts) ret0, _ := ret[0].(TList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) List(opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).List), opts) } // OnChange mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) OnChange(ctx context.Context, name string, sync generic.ObjectHandler[T]) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnChange", ctx, name, sync) } // OnChange indicates an expected call of OnChange. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) OnChange(ctx, name, sync any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnChange", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).OnChange), ctx, name, sync) } // OnRemove mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) OnRemove(ctx context.Context, name string, sync generic.ObjectHandler[T]) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnRemove", ctx, name, sync) } // OnRemove indicates an expected call of OnRemove. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) OnRemove(ctx, name, sync any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnRemove", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).OnRemove), ctx, name, sync) } // Patch mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (T, error) { m.ctrl.T.Helper() varargs := []any{name, pt, data} for _, a := range subresources { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Patch", varargs...) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Patch indicates an expected call of Patch. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Patch(name, pt, data any, subresources ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{name, pt, data}, subresources...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Patch), varargs...) } // Update mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Update(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Update(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Update), arg0) } // UpdateStatus mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) UpdateStatus(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStatus", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateStatus indicates an expected call of UpdateStatus. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) UpdateStatus(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).UpdateStatus), arg0) } // Updater mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Updater() generic.Updater { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Updater") ret0, _ := ret[0].(generic.Updater) return ret0 } // Updater indicates an expected call of Updater. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Updater() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updater", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Updater)) } // Watch mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) Watch(opts v1.ListOptions) (watch.Interface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Watch", opts) ret0, _ := ret[0].(watch.Interface) ret1, _ := ret[1].(error) return ret0, ret1 } // Watch indicates an expected call of Watch. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) Watch(opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).Watch), opts) } // WithImpersonation mocks base method. func (m *MockNonNamespacedControllerInterface[T, TList]) WithImpersonation(impersonate rest.ImpersonationConfig) (generic.NonNamespacedClientInterface[T, TList], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithImpersonation", impersonate) ret0, _ := ret[0].(generic.NonNamespacedClientInterface[T, TList]) ret1, _ := ret[1].(error) return ret0, ret1 } // WithImpersonation indicates an expected call of WithImpersonation. func (mr *MockNonNamespacedControllerInterfaceMockRecorder[T, TList]) WithImpersonation(impersonate any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithImpersonation", reflect.TypeOf((*MockNonNamespacedControllerInterface[T, TList])(nil).WithImpersonation), impersonate) } // MockClientInterface is a mock of ClientInterface interface. type MockClientInterface[T generic.RuntimeMetaObject, TList runtime.Object] struct { ctrl *gomock.Controller recorder *MockClientInterfaceMockRecorder[T, TList] isgomock struct{} } // MockClientInterfaceMockRecorder is the mock recorder for MockClientInterface. type MockClientInterfaceMockRecorder[T generic.RuntimeMetaObject, TList runtime.Object] struct { mock *MockClientInterface[T, TList] } // NewMockClientInterface creates a new mock instance. func NewMockClientInterface[T generic.RuntimeMetaObject, TList runtime.Object](ctrl *gomock.Controller) *MockClientInterface[T, TList] { mock := &MockClientInterface[T, TList]{ctrl: ctrl} mock.recorder = &MockClientInterfaceMockRecorder[T, TList]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockClientInterface[T, TList]) EXPECT() *MockClientInterfaceMockRecorder[T, TList] { return m.recorder } // Create mocks base method. func (m *MockClientInterface[T, TList]) Create(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. func (mr *MockClientInterfaceMockRecorder[T, TList]) Create(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClientInterface[T, TList])(nil).Create), arg0) } // Delete mocks base method. func (m *MockClientInterface[T, TList]) Delete(namespace, name string, options *v1.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", namespace, name, options) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockClientInterfaceMockRecorder[T, TList]) Delete(namespace, name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClientInterface[T, TList])(nil).Delete), namespace, name, options) } // DeleteCollection mocks base method. func (m *MockClientInterface[T, TList]) DeleteCollection(namespace string, deleteOpts v1.DeleteOptions, listOpts v1.ListOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteCollection", namespace, deleteOpts, listOpts) ret0, _ := ret[0].(error) return ret0 } // DeleteCollection indicates an expected call of DeleteCollection. func (mr *MockClientInterfaceMockRecorder[T, TList]) DeleteCollection(namespace, deleteOpts, listOpts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockClientInterface[T, TList])(nil).DeleteCollection), namespace, deleteOpts, listOpts) } // Get mocks base method. func (m *MockClientInterface[T, TList]) Get(namespace, name string, options v1.GetOptions) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", namespace, name, options) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockClientInterfaceMockRecorder[T, TList]) Get(namespace, name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClientInterface[T, TList])(nil).Get), namespace, name, options) } // List mocks base method. func (m *MockClientInterface[T, TList]) List(namespace string, opts v1.ListOptions) (TList, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", namespace, opts) ret0, _ := ret[0].(TList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockClientInterfaceMockRecorder[T, TList]) List(namespace, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClientInterface[T, TList])(nil).List), namespace, opts) } // Patch mocks base method. func (m *MockClientInterface[T, TList]) Patch(namespace, name string, pt types.PatchType, data []byte, subresources ...string) (T, error) { m.ctrl.T.Helper() varargs := []any{namespace, name, pt, data} for _, a := range subresources { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Patch", varargs...) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Patch indicates an expected call of Patch. func (mr *MockClientInterfaceMockRecorder[T, TList]) Patch(namespace, name, pt, data any, subresources ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{namespace, name, pt, data}, subresources...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClientInterface[T, TList])(nil).Patch), varargs...) } // Update mocks base method. func (m *MockClientInterface[T, TList]) Update(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. func (mr *MockClientInterfaceMockRecorder[T, TList]) Update(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClientInterface[T, TList])(nil).Update), arg0) } // UpdateStatus mocks base method. func (m *MockClientInterface[T, TList]) UpdateStatus(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStatus", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateStatus indicates an expected call of UpdateStatus. func (mr *MockClientInterfaceMockRecorder[T, TList]) UpdateStatus(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockClientInterface[T, TList])(nil).UpdateStatus), arg0) } // Watch mocks base method. func (m *MockClientInterface[T, TList]) Watch(namespace string, opts v1.ListOptions) (watch.Interface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Watch", namespace, opts) ret0, _ := ret[0].(watch.Interface) ret1, _ := ret[1].(error) return ret0, ret1 } // Watch indicates an expected call of Watch. func (mr *MockClientInterfaceMockRecorder[T, TList]) Watch(namespace, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockClientInterface[T, TList])(nil).Watch), namespace, opts) } // WithImpersonation mocks base method. func (m *MockClientInterface[T, TList]) WithImpersonation(impersonate rest.ImpersonationConfig) (generic.ClientInterface[T, TList], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithImpersonation", impersonate) ret0, _ := ret[0].(generic.ClientInterface[T, TList]) ret1, _ := ret[1].(error) return ret0, ret1 } // WithImpersonation indicates an expected call of WithImpersonation. func (mr *MockClientInterfaceMockRecorder[T, TList]) WithImpersonation(impersonate any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithImpersonation", reflect.TypeOf((*MockClientInterface[T, TList])(nil).WithImpersonation), impersonate) } // MockNonNamespacedClientInterface is a mock of NonNamespacedClientInterface interface. type MockNonNamespacedClientInterface[T generic.RuntimeMetaObject, TList runtime.Object] struct { ctrl *gomock.Controller recorder *MockNonNamespacedClientInterfaceMockRecorder[T, TList] isgomock struct{} } // MockNonNamespacedClientInterfaceMockRecorder is the mock recorder for MockNonNamespacedClientInterface. type MockNonNamespacedClientInterfaceMockRecorder[T generic.RuntimeMetaObject, TList runtime.Object] struct { mock *MockNonNamespacedClientInterface[T, TList] } // NewMockNonNamespacedClientInterface creates a new mock instance. func NewMockNonNamespacedClientInterface[T generic.RuntimeMetaObject, TList runtime.Object](ctrl *gomock.Controller) *MockNonNamespacedClientInterface[T, TList] { mock := &MockNonNamespacedClientInterface[T, TList]{ctrl: ctrl} mock.recorder = &MockNonNamespacedClientInterfaceMockRecorder[T, TList]{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockNonNamespacedClientInterface[T, TList]) EXPECT() *MockNonNamespacedClientInterfaceMockRecorder[T, TList] { return m.recorder } // Create mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) Create(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) Create(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).Create), arg0) } // Delete mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) Delete(name string, options *v1.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", name, options) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) Delete(name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).Delete), name, options) } // Get mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) Get(name string, options v1.GetOptions) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", name, options) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) Get(name, options any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).Get), name, options) } // List mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) List(opts v1.ListOptions) (TList, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", opts) ret0, _ := ret[0].(TList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) List(opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).List), opts) } // Patch mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (T, error) { m.ctrl.T.Helper() varargs := []any{name, pt, data} for _, a := range subresources { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Patch", varargs...) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Patch indicates an expected call of Patch. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) Patch(name, pt, data any, subresources ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{name, pt, data}, subresources...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).Patch), varargs...) } // Update mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) Update(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) Update(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).Update), arg0) } // UpdateStatus mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) UpdateStatus(arg0 T) (T, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStatus", arg0) ret0, _ := ret[0].(T) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateStatus indicates an expected call of UpdateStatus. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) UpdateStatus(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).UpdateStatus), arg0) } // Watch mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) Watch(opts v1.ListOptions) (watch.Interface, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Watch", opts) ret0, _ := ret[0].(watch.Interface) ret1, _ := ret[1].(error) return ret0, ret1 } // Watch indicates an expected call of Watch. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) Watch(opts any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).Watch), opts) } // WithImpersonation mocks base method. func (m *MockNonNamespacedClientInterface[T, TList]) WithImpersonation(impersonate rest.ImpersonationConfig) (generic.NonNamespacedClientInterface[T, TList], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WithImpersonation", impersonate) ret0, _ := ret[0].(generic.NonNamespacedClientInterface[T, TList]) ret1, _ := ret[1].(error) return ret0, ret1 } // WithImpersonation indicates an expected call of WithImpersonation. func (mr *MockNonNamespacedClientInterfaceMockRecorder[T, TList]) WithImpersonation(impersonate any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithImpersonation", reflect.TypeOf((*MockNonNamespacedClientInterface[T, TList])(nil).WithImpersonation), impersonate) } ================================================ FILE: pkg/generic/fake/fake_test.go ================================================ package fake_test import ( "testing" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/generic/fake" v1 "k8s.io/api/core/v1" ) // TestInterfaceImplementation is a simple test to verify the fake package is kept up to date. // if this compiles it is valid. func TestInterfaceImplementation(t *testing.T) { var ( _ generic.ControllerInterface[*v1.Secret, *v1.SecretList] = fake.NewMockControllerInterface[*v1.Secret, *v1.SecretList](nil) _ generic.NonNamespacedControllerInterface[*v1.Secret, *v1.SecretList] = fake.NewMockNonNamespacedControllerInterface[*v1.Secret, *v1.SecretList](nil) _ generic.ClientInterface[*v1.Secret, *v1.SecretList] = fake.NewMockClientInterface[*v1.Secret, *v1.SecretList](nil) _ generic.NonNamespacedClientInterface[*v1.Secret, *v1.SecretList] = fake.NewMockNonNamespacedClientInterface[*v1.Secret, *v1.SecretList](nil) _ generic.CacheInterface[*v1.Secret] = fake.NewMockCacheInterface[*v1.Secret](nil) _ generic.NonNamespacedCacheInterface[*v1.Secret] = fake.NewMockNonNamespacedCacheInterface[*v1.Secret](nil) ) } ================================================ FILE: pkg/generic/fake/generate.go ================================================ package fake //go:generate bash -c "tmp=$(mktemp); cp ../controller.go $DOLLAR{tmp} && sed -e 's#^\\t*comparable$DOLLAR#// comparable#' $DOLLAR{tmp} > ../controller.go && mockgen -package fake -destination ./controller.go -source ../controller.go; mv $DOLLAR{tmp} ../controller.go " //go:generate mockgen -package fake -destination ./cache.go -source ../cache.go ================================================ FILE: pkg/generic/generating.go ================================================ package generic import ( "github.com/rancher/wrangler/v3/pkg/apply" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type GeneratingHandlerOptions struct { AllowCrossNamespace bool AllowClusterScoped bool NoOwnerReference bool DynamicLookup bool // UniqueApplyForResourceVersion will skip calling apply if the resource version didn't change from the previous execution UniqueApplyForResourceVersion bool } func ConfigureApplyForObject(apply apply.Apply, obj metav1.Object, opts *GeneratingHandlerOptions) apply.Apply { if opts == nil { opts = &GeneratingHandlerOptions{} } if opts.DynamicLookup { apply = apply.WithDynamicLookup() } if opts.NoOwnerReference { apply = apply.WithSetOwnerReference(true, false) } if opts.AllowCrossNamespace && !opts.AllowClusterScoped { apply = apply. WithDefaultNamespace(obj.GetNamespace()). WithListerNamespace(obj.GetNamespace()) } if !opts.AllowClusterScoped { apply = apply.WithRestrictClusterScoped() } return apply } ================================================ FILE: pkg/generic/generating_test.go ================================================ package generic_test import ( "context" "testing" "github.com/rancher/wrangler/v3/pkg/generic" "go.uber.org/mock/gomock" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "github.com/rancher/wrangler/v3/pkg/apply" fakeapply "github.com/rancher/wrangler/v3/pkg/apply/fake" v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" fake2 "github.com/rancher/wrangler/v3/pkg/generic/fake" ) func TestUniqueApplyForResourceVersion(t *testing.T) { const numOfHandlerCalls = 3 type args struct { opts *generic.GeneratingHandlerOptions } tests := []struct { name string args args expectedApplyCount int }{ { name: "disabled", args: args{ opts: &generic.GeneratingHandlerOptions{UniqueApplyForResourceVersion: false}, }, expectedApplyCount: numOfHandlerCalls, }, { name: "enabled", args: args{ opts: &generic.GeneratingHandlerOptions{UniqueApplyForResourceVersion: true}, }, expectedApplyCount: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) applySpy := &fakeapply.FakeApply{} h := setupTestHandler(ctrl, applySpy, tt.args.opts) if h == nil { t.Fatal("error setting up test handler") } service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "testsvc", Namespace: "testns", ResourceVersion: "unchanged"}, Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}}, Status: corev1.ServiceStatus{ Conditions: []metav1.Condition{}, }, } key := service.Namespace + "/" + service.Name for i := 0; i < numOfHandlerCalls; i++ { if _, err := h(key, service); err != nil { t.Error(err) } } if got, want := applySpy.Count, tt.expectedApplyCount; got != want { t.Errorf("Apply calls count = %v, want %v", got, want) } service.ResourceVersion = "changed" if _, err := h(key, service); err != nil { t.Error(err) } if got, want := applySpy.Count, tt.expectedApplyCount+1; got != want { t.Errorf("Apply calls count = %v, want %v", got, want) } }) } } func setupTestHandler(ctrl *gomock.Controller, apply apply.Apply, opts *generic.GeneratingHandlerOptions) (handler generic.Handler) { const handlerName = "test" controller := fake2.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) controller.EXPECT().GroupVersionKind() controller.EXPECT().OnChange(gomock.Any(), gomock.Any(), gomock.Any()) controller.EXPECT(). AddGenericHandler(gomock.Any(), gomock.Eq(handlerName), gomock.Any()). Do(func(_ context.Context, _ string, h generic.Handler) { handler = h }).Times(1) v1.RegisterServiceGeneratingHandler(context.Background(), controller, apply, "", handlerName, func(svc *corev1.Service, status corev1.ServiceStatus) (objs []runtime.Object, newstatus corev1.ServiceStatus, err error) { return []runtime.Object{serviceToEndpoint(svc)}, status, nil }, opts) return } func serviceToEndpoint(svc *corev1.Service) *corev1.Endpoints { var ports []corev1.EndpointPort for _, port := range svc.Spec.Ports { ports = append(ports, corev1.EndpointPort{ Name: port.Name, Port: port.Port, Protocol: port.Protocol, AppProtocol: port.AppProtocol, }) } return &corev1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: svc.Name, Namespace: svc.Namespace, }, Subsets: []corev1.EndpointSubset{ {Ports: ports}, }, } } ================================================ FILE: pkg/generic/remove.go ================================================ package generic import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" ) var ( finalizerKey = "wrangler.cattle.io/" ) type Updater func(runtime.Object) (runtime.Object, error) type objectLifecycleAdapter struct { name string handler Handler updater Updater } func NewRemoveHandler(name string, updater Updater, handler Handler) Handler { o := objectLifecycleAdapter{ name: name, handler: handler, updater: updater, } return o.sync } func (o *objectLifecycleAdapter) sync(key string, obj runtime.Object) (runtime.Object, error) { if obj == nil { return nil, nil } metadata, err := meta.Accessor(obj) if err != nil { return obj, err } if metadata.GetDeletionTimestamp() == nil { return o.addFinalizer(obj) } if !o.hasFinalizer(obj) { return obj, nil } newObj, err := o.handler(key, obj) if err != nil { return newObj, err } if newObj != nil { obj = newObj } return o.removeFinalizer(obj) } func (o *objectLifecycleAdapter) constructFinalizerKey() string { return finalizerKey + o.name } func (o *objectLifecycleAdapter) hasFinalizer(obj runtime.Object) bool { metadata, err := meta.Accessor(obj) if err != nil { return false } finalizerKey := o.constructFinalizerKey() finalizers := metadata.GetFinalizers() for _, finalizer := range finalizers { if finalizer == finalizerKey { return true } } return false } func (o *objectLifecycleAdapter) removeFinalizer(obj runtime.Object) (runtime.Object, error) { if !o.hasFinalizer(obj) { return obj, nil } obj = obj.DeepCopyObject() metadata, err := meta.Accessor(obj) if err != nil { return obj, err } finalizerKey := o.constructFinalizerKey() finalizers := metadata.GetFinalizers() var newFinalizers []string for k, v := range finalizers { if v != finalizerKey { continue } newFinalizers = append(finalizers[:k], finalizers[k+1:]...) } metadata.SetFinalizers(newFinalizers) return o.updater(obj) } func (o *objectLifecycleAdapter) addFinalizer(obj runtime.Object) (runtime.Object, error) { if o.hasFinalizer(obj) { return obj, nil } obj = obj.DeepCopyObject() metadata, err := meta.Accessor(obj) if err != nil { return nil, err } metadata.SetFinalizers(append(metadata.GetFinalizers(), o.constructFinalizerKey())) return o.updater(obj) } ================================================ FILE: pkg/generic/remove_test.go ================================================ package generic import ( "fmt" "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) func Test_objectLifecycleAdapter_sync(t *testing.T) { const handlerName = "test" type args struct { key string obj runtime.Object } tests := []struct { name string args args wantErr bool wantFunc func(runtime.Object) error wantHandlerCount int wantUpdaterCount int }{ { name: "nil object", args: args{ "objectkey", nil, }, wantFunc: func(obj runtime.Object) error { if obj != nil { return fmt.Errorf("obj should be nil") } return nil }, }, { name: "new object", args: args{ "test/service", &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "service"}, }, }, wantUpdaterCount: 1, // update to add finalizer }, { name: "existing object with finalizer", args: args{ "test/service", &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", Name: "service", Finalizers: []string{"finalizer", finalizerKey + handlerName}, }, }, }, wantUpdaterCount: 0, // finalizer already set, no need to update }, { name: "deleted object with finalizer", args: args{ key: "test/service", obj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", Name: "service", Finalizers: []string{"finalizer", finalizerKey + handlerName}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, }, }, wantHandlerCount: 1, wantUpdaterCount: 1, // update removing finalizer }, { name: "deleted object without finalizer", args: args{ key: "test/service", obj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", Name: "service", DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, }, }, wantHandlerCount: 0, wantUpdaterCount: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var handlerCount, updaterCount int updater := func(obj runtime.Object) (runtime.Object, error) { updaterCount++ return obj, nil } handler := func(key string, obj runtime.Object) (runtime.Object, error) { handlerCount++ return obj, nil } sync := NewRemoveHandler("test", updater, handler) got, err := sync(tt.args.key, tt.args.obj) if (err != nil) != tt.wantErr { t.Errorf("sync() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantFunc != nil { if err := tt.wantFunc(got); err != nil { t.Errorf("sync() unexpected value = %v, err: %v", got, err) } } if handlerCount != tt.wantHandlerCount { t.Errorf("handlerCount = %v, want %v", handlerCount, tt.wantHandlerCount) } if updaterCount != tt.wantUpdaterCount { t.Errorf("updaterCount = %v, want %v", updaterCount, tt.wantUpdaterCount) } }) } } ================================================ FILE: pkg/genericcondition/condition.go ================================================ package genericcondition import v1 "k8s.io/api/core/v1" type GenericCondition struct { // Type of cluster condition. Type string `json:"type"` // Status of the condition, one of True, False, Unknown. Status v1.ConditionStatus `json:"status"` // The last time this condition was updated. LastUpdateTime string `json:"lastUpdateTime,omitempty"` // Last time the condition transitioned from one status to another. LastTransitionTime string `json:"lastTransitionTime,omitempty"` // The reason for the condition's last transition. Reason string `json:"reason,omitempty"` // Human-readable message indicating details about last transition Message string `json:"message,omitempty"` } ================================================ FILE: pkg/gvk/detect.go ================================================ package gvk import ( "encoding/json" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func Detect(obj []byte) (schema.GroupVersionKind, bool, error) { partial := v1.PartialObjectMetadata{} if err := json.Unmarshal(obj, &partial); err != nil { return schema.GroupVersionKind{}, false, err } result := partial.GetObjectKind().GroupVersionKind() ok := result.Kind != "" && result.Version != "" return result, ok, nil } ================================================ FILE: pkg/gvk/get.go ================================================ package gvk import ( "fmt" "github.com/rancher/wrangler/v3/pkg/schemes" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) func Get(obj runtime.Object) (schema.GroupVersionKind, error) { gvk := obj.GetObjectKind().GroupVersionKind() if gvk.Kind != "" { return gvk, nil } gvks, _, err := schemes.All.ObjectKinds(obj) if err != nil { return schema.GroupVersionKind{}, fmt.Errorf("failed to find gvk for %T, you may need to import the wrangler generated controller package: %w", obj, err) } if len(gvks) == 0 { return schema.GroupVersionKind{}, fmt.Errorf("failed to find gvk for %T", obj) } return gvks[0], nil } func Set(objs ...runtime.Object) error { for _, obj := range objs { if err := setObject(obj); err != nil { return err } } return nil } func setObject(obj runtime.Object) error { gvk := obj.GetObjectKind().GroupVersionKind() if gvk.Kind != "" { return nil } gvks, _, err := schemes.All.ObjectKinds(obj) if err != nil { return err } if len(gvks) == 0 { return nil } kind := obj.GetObjectKind() kind.SetGroupVersionKind(gvks[0]) return nil } ================================================ FILE: pkg/k8scheck/wait.go ================================================ package k8scheck import ( "context" "fmt" "time" "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) func Wait(ctx context.Context, config rest.Config) error { client, err := kubernetes.NewForConfig(&config) if err != nil { return err } for { _, err := client.Discovery().ServerVersion() if err == nil { break } logrus.Infof("Waiting for server to become available: %v", err) select { case <-ctx.Done(): return fmt.Errorf("startup canceled") case <-time.After(2 * time.Second): } } return nil } ================================================ FILE: pkg/kstatus/kstatus.go ================================================ package kstatus import "github.com/rancher/wrangler/v3/pkg/condition" // Conditions read by the kstatus package const ( Reconciling = condition.Cond("Reconciling") Stalled = condition.Cond("Stalled") ) func SetError(obj interface{}, message string) { Reconciling.False(obj) Reconciling.Message(obj, "") Reconciling.Reason(obj, "") Stalled.True(obj) Stalled.Reason(obj, string(Stalled)) Stalled.Message(obj, message) } func SetTransitioning(obj interface{}, message string) { Reconciling.True(obj) Reconciling.Message(obj, message) Reconciling.Reason(obj, string(Reconciling)) Stalled.False(obj) Stalled.Reason(obj, "") Stalled.Message(obj, "") } func SetActive(obj interface{}) { Reconciling.False(obj) Reconciling.Message(obj, "") Reconciling.Reason(obj, "") Stalled.False(obj) Stalled.Reason(obj, "") Stalled.Message(obj, "") } ================================================ FILE: pkg/kubeconfig/loader.go ================================================ package kubeconfig import ( "io" "os" "path/filepath" "k8s.io/client-go/tools/clientcmd" ) func GetNonInteractiveClientConfig(kubeConfig string) clientcmd.ClientConfig { return GetClientConfig(kubeConfig, nil) } func GetNonInteractiveClientConfigWithContext(kubeConfig, currentContext string) clientcmd.ClientConfig { return GetClientConfigWithContext(kubeConfig, currentContext, nil) } func GetInteractiveClientConfig(kubeConfig string) clientcmd.ClientConfig { return GetClientConfig(kubeConfig, os.Stdin) } func GetClientConfigWithContext(kubeConfig, currentContext string, reader io.Reader) clientcmd.ClientConfig { loadingRules := GetLoadingRules(kubeConfig) overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults, CurrentContext: currentContext} return clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, reader) } func GetClientConfig(kubeConfig string, reader io.Reader) clientcmd.ClientConfig { return GetClientConfigWithContext(kubeConfig, "", reader) } func GetLoadingRules(kubeConfig string) *clientcmd.ClientConfigLoadingRules { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig if kubeConfig != "" { loadingRules.ExplicitPath = kubeConfig } var otherFiles []string homeDir, err := os.UserHomeDir() if err == nil { otherFiles = append(otherFiles, filepath.Join(homeDir, ".kube", "k3s.yaml")) } otherFiles = append(otherFiles, "/etc/rancher/k3s/k3s.yaml") loadingRules.Precedence = append(loadingRules.Precedence, canRead(otherFiles)...) return loadingRules } func canRead(files []string) (result []string) { for _, f := range files { _, err := os.ReadFile(f) if err == nil { result = append(result, f) } } return } ================================================ FILE: pkg/kv/split.go ================================================ package kv import "strings" // Like split but if there is only one item return "", item func RSplit(s, sep string) (string, string) { parts := strings.SplitN(s, sep, 2) if len(parts) == 1 { return "", strings.TrimSpace(parts[0]) } return strings.TrimSpace(parts[0]), strings.TrimSpace(safeIndex(parts, 1)) } func Split(s, sep string) (string, string) { parts := strings.SplitN(s, sep, 2) return strings.TrimSpace(parts[0]), strings.TrimSpace(safeIndex(parts, 1)) } func SplitLast(s, sep string) (string, string) { idx := strings.LastIndex(s, sep) if idx > -1 { return strings.TrimSpace(s[:idx]), strings.TrimSpace(s[idx+1:]) } return s, "" } func SplitMap(s, sep string) map[string]string { return SplitMapFromSlice(strings.Split(s, sep)) } func SplitMapFromSlice(parts []string) map[string]string { result := map[string]string{} for _, part := range parts { k, v := Split(part, "=") result[k] = v } return result } func safeIndex(parts []string, idx int) string { if len(parts) <= idx { return "" } return parts[idx] } ================================================ FILE: pkg/leader/leader.go ================================================ package leader import ( "context" "fmt" "os" "time" "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" ) type Callback func(cb context.Context) const devModeEnvKey = "CATTLE_DEV_MODE" const leaseDurationEnvKey = "CATTLE_ELECTION_LEASE_DURATION" const renewDeadlineEnvKey = "CATTLE_ELECTION_RENEW_DEADLINE" const retryPeriodEnvKey = "CATTLE_ELECTION_RETRY_PERIOD" const defaultLeaseDuration = 45 * time.Second const defaultRenewDeadline = 30 * time.Second const defaultRetryPeriod = 2 * time.Second const developmentLeaseDuration = 45 * time.Hour const developmentRenewDeadline = 30 * time.Hour func RunOrDie(ctx context.Context, namespace, name string, client kubernetes.Interface, cb Callback) { if namespace == "" { namespace = "kube-system" } err := run(ctx, namespace, name, client, cb) if err != nil { logrus.Fatalf("Failed to start leader election for %s", name) } panic("Failed to start leader election for " + name) } func run(ctx context.Context, namespace, name string, client kubernetes.Interface, cb Callback) error { id, err := os.Hostname() if err != nil { return err } rl, err := resourcelock.New(resourcelock.LeasesResourceLock, namespace, name, client.CoreV1(), client.CoordinationV1(), resourcelock.ResourceLockConfig{ Identity: id, }) if err != nil { logrus.Fatalf("error creating leader lock for %s: %v", name, err) } cbs := leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { go cb(ctx) }, OnStoppedLeading: func() { select { case <-ctx.Done(): // The context has been canceled or is otherwise complete. // This is a request to terminate. Exit 0. // Exiting cleanly is useful when the context is canceled // so that Kubernetes doesn't record it exiting in error // when the exit was requested. For example, the wrangler-cli // package sets up a context that cancels when SIGTERM is // sent in. If a node is shut down this is the type of signal // sent. In that case you want the 0 exit code to mark it as // complete so that everything comes back up correctly after // a restart. // The pattern found here can be found inside the kube-scheduler. logrus.Info("requested to terminate, exiting") os.Exit(0) default: logrus.Fatalf("leaderelection lost for %s", name) } }, } config, err := computeConfig(rl, cbs) if err != nil { return err } leaderelection.RunOrDie(ctx, *config) panic("unreachable") } func computeConfig(rl resourcelock.Interface, cbs leaderelection.LeaderCallbacks) (*leaderelection.LeaderElectionConfig, error) { leaseDuration := defaultLeaseDuration renewDeadline := defaultRenewDeadline retryPeriod := defaultRetryPeriod var err error if d := os.Getenv(devModeEnvKey); d != "" { leaseDuration = developmentLeaseDuration renewDeadline = developmentRenewDeadline } if d := os.Getenv(leaseDurationEnvKey); d != "" { leaseDuration, err = time.ParseDuration(d) if err != nil { return nil, fmt.Errorf("%s value [%s] is not a valid duration: %w", leaseDurationEnvKey, d, err) } } if d := os.Getenv(renewDeadlineEnvKey); d != "" { renewDeadline, err = time.ParseDuration(d) if err != nil { return nil, fmt.Errorf("%s value [%s] is not a valid duration: %w", renewDeadlineEnvKey, d, err) } } if d := os.Getenv(retryPeriodEnvKey); d != "" { retryPeriod, err = time.ParseDuration(d) if err != nil { return nil, fmt.Errorf("%s value [%s] is not a valid duration: %w", retryPeriodEnvKey, d, err) } } return &leaderelection.LeaderElectionConfig{ Lock: rl, LeaseDuration: leaseDuration, RenewDeadline: renewDeadline, RetryPeriod: retryPeriod, Callbacks: cbs, ReleaseOnCancel: true, }, nil } ================================================ FILE: pkg/leader/leader_test.go ================================================ package leader import ( "os" "reflect" "testing" "time" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" ) func Test_computeConfig(t *testing.T) { type args struct { rl resourcelock.Interface cbs leaderelection.LeaderCallbacks } type env struct { key string value string } tests := []struct { name string args args envs []env want *leaderelection.LeaderElectionConfig wantErr bool }{ { name: "all defaults", args: args{ rl: nil, cbs: leaderelection.LeaderCallbacks{}, }, envs: []env{}, want: &leaderelection.LeaderElectionConfig{ Lock: nil, LeaseDuration: defaultLeaseDuration, RenewDeadline: defaultRenewDeadline, RetryPeriod: defaultRetryPeriod, Callbacks: leaderelection.LeaderCallbacks{}, ReleaseOnCancel: true, }, wantErr: false, }, { name: "dev mode", args: args{ rl: nil, cbs: leaderelection.LeaderCallbacks{}, }, envs: []env{ {key: devModeEnvKey, value: "true"}, }, want: &leaderelection.LeaderElectionConfig{ Lock: nil, LeaseDuration: developmentLeaseDuration, RenewDeadline: developmentRenewDeadline, RetryPeriod: defaultRetryPeriod, Callbacks: leaderelection.LeaderCallbacks{}, ReleaseOnCancel: true, }, wantErr: false, }, { name: "all overridden", args: args{ rl: nil, cbs: leaderelection.LeaderCallbacks{}, }, envs: []env{ {key: devModeEnvKey, value: "true"}, {key: leaseDurationEnvKey, value: "1s"}, {key: renewDeadlineEnvKey, value: "2s"}, {key: retryPeriodEnvKey, value: "3m"}, }, want: &leaderelection.LeaderElectionConfig{ Lock: nil, LeaseDuration: time.Second, RenewDeadline: 2 * time.Second, RetryPeriod: 3 * time.Minute, Callbacks: leaderelection.LeaderCallbacks{}, ReleaseOnCancel: true, }, wantErr: false, }, { name: "unparseable lease duration", args: args{ rl: nil, cbs: leaderelection.LeaderCallbacks{}, }, envs: []env{ {key: leaseDurationEnvKey, value: "bomb"}, {key: renewDeadlineEnvKey, value: "2s"}, {key: retryPeriodEnvKey, value: "3m"}, }, want: nil, wantErr: true, }, { name: "unparseable renew deadline", args: args{ rl: nil, cbs: leaderelection.LeaderCallbacks{}, }, envs: []env{ {key: leaseDurationEnvKey, value: "1s"}, {key: renewDeadlineEnvKey, value: "bomb"}, {key: retryPeriodEnvKey, value: "3m"}, }, want: nil, wantErr: true, }, { name: "unparseable retry period", args: args{ rl: nil, cbs: leaderelection.LeaderCallbacks{}, }, envs: []env{ {key: leaseDurationEnvKey, value: "1s"}, {key: renewDeadlineEnvKey, value: "2s"}, {key: retryPeriodEnvKey, value: "bomb"}, }, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, e := range []string{leaseDurationEnvKey, renewDeadlineEnvKey, retryPeriodEnvKey} { err := os.Unsetenv(e) if err != nil { t.Errorf("could not Unsetenv: %v", err) return } } for _, e := range tt.envs { err := os.Setenv(e.key, e.value) if err != nil { t.Errorf("could not SetEnv: %v", err) return } } got, err := computeConfig(tt.args.rl, tt.args.cbs) if (err != nil) != tt.wantErr { t.Errorf("computeConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("computeConfig() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/leader/manager.go ================================================ package leader import ( "context" "sync" "time" "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" ) type Manager struct { sync.Mutex leaderChan chan struct{} leaderStarted bool leaderCTX context.Context namespace string name string k8s kubernetes.Interface } func NewManager(namespace, name string, k8s kubernetes.Interface) *Manager { return &Manager{ leaderChan: make(chan struct{}), namespace: namespace, name: name, k8s: k8s, } } func (m *Manager) Start(ctx context.Context) { m.Lock() defer m.Unlock() if m.leaderStarted { return } m.leaderStarted = true go RunOrDie(ctx, m.namespace, m.name, m.k8s, func(ctx context.Context) { m.leaderCTX = ctx close(m.leaderChan) }) } // OnLeaderOrDie this function will be called when leadership is acquired or die if failed func (m *Manager) OnLeaderOrDie(name string, f func(ctx context.Context) error) { go func() { <-m.leaderChan if err := f(m.leaderCTX); err != nil { logrus.Fatalf("%s leader func failed be executed: %v", name, err) } else { logrus.Infof("%s leader func executed successfully", name) } }() } // OnLeader this function will be called when leadership is acquired. func (m *Manager) OnLeader(f func(ctx context.Context) error) { go func() { <-m.leaderChan for { if err := f(m.leaderCTX); err != nil { logrus.Errorf("failed to call leader func: %v", err) time.Sleep(5 * time.Second) continue } break } }() } ================================================ FILE: pkg/merr/error.go ================================================ package merr import "bytes" type Errors []error func (e Errors) Err() error { return NewErrors(e...) } func (e Errors) Error() string { buf := bytes.NewBuffer(nil) for _, err := range e { if buf.Len() > 0 { buf.WriteString(", ") } buf.WriteString(err.Error()) } return buf.String() } func NewErrors(inErrors ...error) error { var errors []error for _, err := range inErrors { if err != nil { errors = append(errors, err) } } if len(errors) == 0 { return nil } else if len(errors) == 1 { return errors[0] } return Errors(errors) } ================================================ FILE: pkg/name/name.go ================================================ package name import ( "crypto/md5" "crypto/sha256" "encoding/hex" "fmt" "strings" ) // GuessPluralName attempts to pluralize a noun. func GuessPluralName(name string) string { if name == "" { return name } if strings.EqualFold(name, "Endpoints") { return name } if suffix(name, "s") || suffix(name, "ch") || suffix(name, "x") || suffix(name, "sh") { return name + "es" } if suffix(name, "f") || suffix(name, "fe") { return name + "ves" } if suffix(name, "y") && len(name) > 2 && !strings.ContainsAny(name[len(name)-2:len(name)-1], "[aeiou]") { return name[0:len(name)-1] + "ies" } return name + "s" } func suffix(str, end string) bool { return strings.HasSuffix(str, end) } // Limit the length of a string to count characters. If the string's length is // greater or equal to count, it will be truncated and a hash will be appended // to the end. // Warning: runtime error for count <= 5: https://go.dev/play/p/UAbpZIOvIYo func Limit(s string, count int) string { if len(s) < count { return s } return fmt.Sprintf("%s-%s", s[:count-6], Hex(s, 5)) } // Hex gets the checksum of s, encodes it to hexadecimal and returns the first n characters of that hexadecimal. // Warning: runtime error for n > 32 or n < 0. func Hex(s string, n int) string { h := md5.Sum([]byte(s)) d := hex.EncodeToString(h[:]) return d[:n] } // SafeConcatName concatenates the given strings and ensures the returned name is under 64 characters // by cutting the string off at 57 characters and setting the last 6 with an encoded version of the concatenated string. func SafeConcatName(name ...string) string { fullPath := strings.Join(name, "-") if len(fullPath) < 64 { return fullPath } digest := sha256.Sum256([]byte(fullPath)) // since we cut the string in the middle, the last char may not be compatible with what is expected in k8s // we are checking and if necessary removing the last char c := fullPath[56] if 'a' <= c && c <= 'z' || '0' <= c && c <= '9' { return fullPath[0:57] + "-" + hex.EncodeToString(digest[0:])[0:5] } return fullPath[0:56] + "-" + hex.EncodeToString(digest[0:])[0:6] } ================================================ FILE: pkg/name/name_test.go ================================================ package name import ( "testing" "github.com/stretchr/testify/assert" ) const ( string32 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" string63 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" string64 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ) // TODO: Improve GuessPluralNames. The rules used do not accurately pluralize nouns. // For now this unit test covers the existing functionality to ensure backwards compatibility. func TestGuessPluralName(t *testing.T) { t.Parallel() tests := []struct { name string nouns map[string]string }{ { name: "Empty string", nouns: map[string]string{"": ""}, }, { name: "Special cases", nouns: map[string]string{"Endpoints": "Endpoints"}, }, { name: "Strings ending in s ch x and sh", nouns: map[string]string{ "iris": "irises", "leech": "leeches", "tax": "taxes", "fish": "fishes", }, }, { name: "Strings ending in f and fe", nouns: map[string]string{ "elf": "elfves", "safe": "safeves", }, }, { name: "Strings ending in y", nouns: map[string]string{ "candy": "candies", "birthday": "birthdays", "turkey": "turkeys", "toy": "toys", "guy": "guys", }, }, { name: "Non-special strings", nouns: map[string]string{ "friend": "friends", "cat": "cats", "dog": "dogs", }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() for k, v := range tt.nouns { got := GuessPluralName(k) assert.Equal(t, v, got, "GuessPluralName(%v) = %v, want %v", k, got, v) } }) } } func TestLimit(t *testing.T) { t.Parallel() tests := []struct { name string s string count int want string wantPanic bool }{ { name: "length of string is less than count", s: "aaaaaa", count: 7, want: "aaaaaa", }, { name: "length of string is equal to count", s: "aaaaaaa", count: 7, want: "a-5d793", }, { name: "length of string is greater than count", s: "aaaaaaaaaaaaaaaaaa", count: 8, want: "aa-2c60c", }, { name: "only hash exists when length of string and count are 6", s: "aaaaaa", count: 6, want: "-0b4e7", }, { name: "empty string", s: "", count: 7, want: "", }, { name: "panic when count <= 5", s: "aaaaaaa", count: 5, wantPanic: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if tt.wantPanic { assert.Panics(t, func() { Limit(tt.s, tt.count) }) return } if got := Limit(tt.s, tt.count); got != tt.want { t.Errorf("Limit() = %v, want %v", got, tt.want) } }) } } func TestHex(t *testing.T) { t.Parallel() tests := []struct { name string s string n int want string wantPanic bool }{ { name: "basic test", s: "aaaaaa", n: 4, want: "0b4e", }, { name: "full checksum", s: "aaaaaa", n: 32, want: "0b4e7a0e5fe84ad35fb5f95b9ceeac79", }, { name: "get more characters than full checksum", s: "aaaaaa", n: 33, wantPanic: true, }, { name: "get negative characters", s: "aaaaaa", n: -1, wantPanic: true, }, { name: "get 0 characters", s: "aaaaaa", n: 0, want: "", }, { name: "empty string", s: "", n: 4, want: "d41d", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if tt.wantPanic { assert.Panics(t, func() { Hex(tt.s, tt.n) }) return } if got := Hex(tt.s, tt.n); got != tt.want { t.Errorf("Hex() = %v, want %v", got, tt.want) } }) } } func TestSafeConcatName(t *testing.T) { t.Parallel() tests := []struct { name string input []string output string }{ { name: "empty input", output: "", }, { name: "single string", input: []string{string63}, output: string63, }, { name: "single long string", input: []string{string64}, output: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-ffe05", }, { name: "concatenate strings", input: []string{"first", "second", "third"}, output: "first-second-third", }, { name: "concatenate past 64 characters", input: []string{string32, string32}, output: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-aaaaaaaaaaaaaaaaaaaaaaaa-da5ed", }, { name: "last character after truncation is not alphanumeric", input: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-aaaaaaa"}, output: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-768c62", }, { name: "last characters after truncation aren't alphanumeric", input: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--aaaaaaa"}, output: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--9e8cfe", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := SafeConcatName(tt.input...); got != tt.output { t.Errorf("SafeConcatName() = %v, want %v", got, tt.output) } }) } } ================================================ FILE: pkg/needacert/needacert.go ================================================ package needacert import ( "bytes" "context" "crypto/tls" "crypto/x509" "fmt" "strings" "time" "github.com/moby/locker" admissionregcontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/admissionregistration.k8s.io/v1" apiextcontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1" corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" "github.com/rancher/wrangler/v3/pkg/gvk" "github.com/rancher/wrangler/v3/pkg/relatedresource" "github.com/rancher/wrangler/v3/pkg/slice" "github.com/sirupsen/logrus" adminregv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierror "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/util/cert" ) var ( SecretAnnotation = "need-a-cert.cattle.io/secret-name" DNSAnnotation = "need-a-cert.cattle.io/dns-name" ) const ( byServiceIndex = "byService" bySecretIndex = "bySecret" ) func Register(ctx context.Context, secrets corecontrollers.SecretController, service corecontrollers.ServiceController, mutatingController admissionregcontrollers.MutatingWebhookConfigurationController, validatingController admissionregcontrollers.ValidatingWebhookConfigurationController, crdController apiextcontrollers.CustomResourceDefinitionController) { h := handler{ secretsCache: secrets.Cache(), secrets: secrets, services: service, serviceCache: service.Cache(), mutatingWebHooks: mutatingController, validatingWebHooks: validatingController, crds: crdController, } mutatingController.Cache().AddIndexer(byServiceIndex, mutatingWebhookServices) validatingController.Cache().AddIndexer(byServiceIndex, validatingWebhookServices) crdController.Cache().AddIndexer(byServiceIndex, crdWebhookServices) service.Cache().AddIndexer(bySecretIndex, serviceSecret) mutatingController.OnChange(ctx, "need-a-cert", h.OnMutationWebhookChange) validatingController.OnChange(ctx, "need-a-cert", h.OnValidatingWebhookChange) crdController.OnChange(ctx, "need-a-cert", h.OnCRDChange) service.OnChange(ctx, "need-a-cert", h.OnService) relatedresource.Watch(ctx, "resolve-service-from-secret", h.resolveServiceFromSecret, service, secrets) } func (h *handler) resolveServiceFromSecret(namespace, name string, obj runtime.Object) ([]relatedresource.Key, error) { if _, ok := obj.(*corev1.Secret); !ok { return nil, nil } services, err := h.serviceCache.GetByIndex(bySecretIndex, namespace+"/"+name) if err != nil { return nil, err } var result []relatedresource.Key for _, service := range services { result = append(result, relatedresource.NewKey(service.GetNamespace(), service.GetName())) } return result, nil } func serviceSecret(obj *corev1.Service) ([]string, error) { secretName := obj.Annotations[SecretAnnotation] if secretName == "" { return nil, nil } return []string{ obj.Namespace + "/" + secretName, }, nil } type handler struct { locker locker.Locker secretsCache corecontrollers.SecretCache secrets corecontrollers.SecretClient serviceCache corecontrollers.ServiceCache services corecontrollers.ServiceController mutatingWebHooks admissionregcontrollers.MutatingWebhookConfigurationController validatingWebHooks admissionregcontrollers.ValidatingWebhookConfigurationController crds apiextcontrollers.CustomResourceDefinitionController } func validatingWebhookServices(obj *adminregv1.ValidatingWebhookConfiguration) (result []string, _ error) { for _, webhook := range obj.Webhooks { if webhook.ClientConfig.Service != nil { result = append(result, webhook.ClientConfig.Service.Namespace+"/"+webhook.ClientConfig.Service.Name) } } return } func crdWebhookServices(obj *apiextv1.CustomResourceDefinition) (result []string, _ error) { if obj.Spec.Conversion != nil && obj.Spec.Conversion.Webhook != nil && obj.Spec.Conversion.Webhook.ClientConfig != nil && obj.Spec.Conversion.Webhook.ClientConfig.Service != nil { return []string{ fmt.Sprintf("%s/%s", obj.Spec.Conversion.Webhook.ClientConfig.Service.Namespace, obj.Spec.Conversion.Webhook.ClientConfig.Service.Name), }, nil } return nil, nil } func mutatingWebhookServices(obj *adminregv1.MutatingWebhookConfiguration) (result []string, _ error) { for _, webhook := range obj.Webhooks { if webhook.ClientConfig.Service != nil { result = append(result, webhook.ClientConfig.Service.Namespace+"/"+webhook.ClientConfig.Service.Name) } } return } func (h *handler) OnMutationWebhookChange(key string, webhook *adminregv1.MutatingWebhookConfiguration) (*adminregv1.MutatingWebhookConfiguration, error) { if webhook == nil { return nil, nil } needUpdate := false for i, webhookConfig := range webhook.Webhooks { if webhookConfig.ClientConfig.Service == nil || webhookConfig.ClientConfig.Service.Name == "" { continue } service, err := h.serviceCache.Get(webhookConfig.ClientConfig.Service.Namespace, webhookConfig.ClientConfig.Service.Name) if apierror.IsNotFound(err) { // OnService will be called when the service is created, which will eventually update the webhook, so no // need to enqueue anything if we don't find the service return webhook, nil } else if err != nil { return nil, err } secret, err := h.generateSecret(service) if err != nil { return nil, err } else if secret == nil { continue } if !bytes.Equal(webhookConfig.ClientConfig.CABundle, secret.Data[corev1.TLSCertKey]) { webhook = webhook.DeepCopy() webhook.Webhooks[i].ClientConfig.CABundle = secret.Data[corev1.TLSCertKey] needUpdate = true } } if needUpdate { logrus.Debugf("Updating MutatingWebhookConfiguration %s/%s", webhook.GetNamespace(), webhook.GetName()) webhook, err := h.mutatingWebHooks.Update(webhook) if err != nil { return webhook, err } } return webhook, nil } func (h *handler) OnValidatingWebhookChange(key string, webhook *adminregv1.ValidatingWebhookConfiguration) (*adminregv1.ValidatingWebhookConfiguration, error) { if webhook == nil { return nil, nil } needUpdate := false for i, webhookConfig := range webhook.Webhooks { if webhookConfig.ClientConfig.Service == nil || webhookConfig.ClientConfig.Service.Name == "" { continue } service, err := h.serviceCache.Get(webhookConfig.ClientConfig.Service.Namespace, webhookConfig.ClientConfig.Service.Name) if apierror.IsNotFound(err) { // OnService will be called when the service is created, which will eventually update the webhook, so no // need to enqueue anything if we don't find the service return webhook, nil } else if err != nil { return nil, err } secret, err := h.generateSecret(service) if err != nil { return nil, err } else if secret == nil { continue } if !bytes.Equal(webhookConfig.ClientConfig.CABundle, secret.Data[corev1.TLSCertKey]) { webhook = webhook.DeepCopy() webhook.Webhooks[i].ClientConfig.CABundle = secret.Data[corev1.TLSCertKey] needUpdate = true } } if needUpdate { logrus.Debugf("Updating ValidatingWebhookConfiguration %s/%s", webhook.GetNamespace(), webhook.GetName()) webhook, err := h.validatingWebHooks.Update(webhook) if err != nil { return webhook, err } } return webhook, nil } func (h *handler) OnService(key string, service *corev1.Service) (*corev1.Service, error) { if service == nil { return service, nil } _, err := h.generateSecret(service) if err != nil { return nil, err } indexKey := service.Namespace + "/" + service.Name mutating, err := h.mutatingWebHooks.Cache().GetByIndex(byServiceIndex, indexKey) if err != nil { return nil, err } for _, mutating := range mutating { h.mutatingWebHooks.Enqueue(mutating.Name) } validating, err := h.validatingWebHooks.Cache().GetByIndex(byServiceIndex, indexKey) if err != nil { return nil, err } for _, validating := range validating { h.validatingWebHooks.Enqueue(validating.Name) } crd, err := h.crds.Cache().GetByIndex(byServiceIndex, indexKey) if err != nil { return nil, err } for _, crd := range crd { h.crds.Enqueue(crd.Name) } return nil, err } func (h *handler) OnCRDChange(key string, crd *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, error) { if crd == nil || crd.Spec.Conversion == nil || crd.Spec.Conversion.Webhook == nil || crd.Spec.Conversion.Webhook.ClientConfig == nil || crd.Spec.Conversion.Webhook.ClientConfig.Service == nil || crd.Spec.Conversion.Webhook.ClientConfig.Service.Name == "" { return crd, nil } service, err := h.serviceCache.Get(crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace, crd.Spec.Conversion.Webhook.ClientConfig.Service.Name) if apierror.IsNotFound(err) { // OnService will be called when the service is created, which will eventually update the CRD, so no // need to enqueue anything if we don't find the service return crd, nil } else if err != nil { return nil, err } secret, err := h.generateSecret(service) if err != nil || secret == nil { return crd, nil } if !bytes.Equal(crd.Spec.Conversion.Webhook.ClientConfig.CABundle, secret.Data[corev1.TLSCertKey]) { crd := crd.DeepCopy() crd.Spec.Conversion.Webhook.ClientConfig.CABundle = secret.Data[corev1.TLSCertKey] return h.crds.Update(crd) } return crd, nil } func (h *handler) generateSecret(service *corev1.Service) (*corev1.Secret, error) { secretName := service.Annotations[SecretAnnotation] if secretName == "" { return nil, nil } lockKey := service.Namespace + "/" + service.Name h.locker.Lock(lockKey) defer h.locker.Unlock(lockKey) dnsNameSet := sets.NewString(service.Name+"."+service.Namespace, service.Name+"."+service.Namespace+".svc", service.Name+"."+service.Namespace+".svc.cluster.local") for k, v := range service.Annotations { if !strings.HasPrefix(k, DNSAnnotation) { continue } dnsNameSet.Insert(v) } // this is sorted dnsNames := dnsNameSet.List() secret, err := h.secretsCache.Get(service.Namespace, secretName) if apierror.IsNotFound(err) { newSecret, err := h.createSecret(service, service.Namespace, secretName, dnsNames) if err != nil { return nil, err } secret, err = h.secrets.Create(newSecret) if apierror.IsAlreadyExists(err) { secret, err = h.secrets.Get(service.Namespace, secretName, metav1.GetOptions{}) if err != nil { return nil, err } } else if err != nil { return nil, err } } else if err != nil { return nil, err } cert, parseErr := parseCert(secret) if parseErr != nil { return nil, parseErr } updated, updateErr := h.updateSecret(service, secret, dnsNames, cert) if updateErr != nil { return nil, updateErr } else if updated != nil { secret, err = h.secrets.Update(updated) if err != nil { return nil, err } } if err := h.scheduleNextCertCheck(service, secret); err != nil { return nil, fmt.Errorf("failed to schedule next cert check: %w", err) } return secret, nil } func (h *handler) updateSecret(owner runtime.Object, secret *corev1.Secret, dnsNames []string, cert *x509.Certificate) (*corev1.Secret, error) { logrus.Debugf("checking cert %s for %s/%s", cert.Subject.CommonName, secret.Namespace, secret.Name) if time.Now().Add(24*60*time.Hour).After(cert.NotAfter) || len(cert.DNSNames) == 0 || !slice.StringsEqual(cert.DNSNames[1:], dnsNames) { logrus.Debugf("regenerating cert %s for %s/%s", cert.Subject.CommonName, secret.Namespace, secret.Name) newSecret, err := h.createSecret(owner, secret.Namespace, secret.Name, dnsNames) if err != nil { return nil, err } secret = secret.DeepCopy() secret.Data = newSecret.Data return secret, nil } logrus.Debugf("cert %s for %s/%s is valid until %s and covers %v", cert.Subject.CommonName, secret.Namespace, secret.Name, cert.NotAfter, cert.DNSNames) return nil, nil } func (h *handler) scheduleNextCertCheck(obj metav1.Object, secret *corev1.Secret) error { cert, err := parseCert(secret) if err != nil { return fmt.Errorf("cannot parse certificate: %w", err) } renewBefore := 24 * 60 * time.Hour nextCheck := time.Until(cert.NotAfter.Add(-renewBefore)) if nextCheck < time.Minute { nextCheck = time.Minute } logrus.Debugf("Next cert check for %s/%s scheduled in %v (expires %v)", obj.GetNamespace(), obj.GetName(), nextCheck.Round(time.Second), cert.NotAfter) h.services.EnqueueAfter(obj.GetNamespace(), obj.GetName(), nextCheck) return nil } func (h *handler) createSecret(owner runtime.Object, ns, name string, dnsNames []string) (*corev1.Secret, error) { cert, key, err := cert.GenerateSelfSignedCertKey(ns+"-"+name, nil, dnsNames) if err != nil { return nil, err } meta, err := meta.Accessor(owner) if err != nil { return nil, err } gvk, err := gvk.Get(owner) if err != nil { return nil, err } ref := metav1.OwnerReference{ Name: meta.GetName(), UID: meta.GetUID(), } ref.APIVersion, ref.Kind = gvk.ToAPIVersionAndKind() return &corev1.Secret{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, OwnerReferences: []metav1.OwnerReference{ref}, }, Data: map[string][]byte{ corev1.TLSCertKey: cert, corev1.TLSPrivateKeyKey: key, }, Type: corev1.SecretTypeTLS, }, nil } func parseCert(secret *corev1.Secret) (*x509.Certificate, error) { if secret == nil || secret.Data == nil { return nil, fmt.Errorf("secret or secret.Data is nil") } tlsPair, err := tls.X509KeyPair(secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey]) if err != nil || len(tlsPair.Certificate) == 0 { return nil, fmt.Errorf("failed to load TLS keypair: %w", err) } cert, err := x509.ParseCertificate(tlsPair.Certificate[0]) if err != nil { return nil, fmt.Errorf("failed to parse X509 certificate: %w", err) } return cert, nil } ================================================ FILE: pkg/needacert/needacert_test.go ================================================ package needacert import ( "bytes" "fmt" "math" "testing" "time" "github.com/rancher/wrangler/v3/pkg/generic/fake" "go.uber.org/mock/gomock" "github.com/stretchr/testify/assert" adminregv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierror "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/cert" ) func TestCreateSecret(t *testing.T) { h := &handler{} service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", }, } dnsNames := []string{"svc.ns", "svc.ns.svc"} secret, err := h.createSecret(service, "ns", "mysecret", dnsNames) assert.NoError(t, err) assert.Equal(t, "mysecret", secret.Name) assert.Equal(t, "ns", secret.Namespace) assert.Equal(t, corev1.SecretTypeTLS, secret.Type) assert.NotEmpty(t, secret.Data[corev1.TLSCertKey]) assert.NotEmpty(t, secret.Data[corev1.TLSPrivateKeyKey]) } func TestUpdateSecret_ExpiredCert_ManyParallel(t *testing.T) { const runs = 50 for i := 0; i < runs; i++ { t.Run(fmt.Sprintf("run-%d", i), func(t *testing.T) { t.Parallel() h := &handler{} service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", }, } dnsNames := []string{"svc.ns", "svc.ns.svc"} certPEM, keyPEM, err := cert.GenerateSelfSignedCertKeyWithOptions(cert.SelfSignedCertKeyOptions{ Host: "ns-mysecret", AlternateDNS: dnsNames, MaxAge: 1 * time.Second, }) assert.NoError(t, err) existingCert, err := cert.ParseCertsPEM(certPEM) assert.NoError(t, err) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } time.Sleep(2 * time.Second) updated, err := h.updateSecret(service, secret, dnsNames, existingCert[0]) assert.NoError(t, err) assert.NotNil(t, updated) }) } } func TestGenerateSecret_NoAnnotation(t *testing.T) { h := &handler{} service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{}, }, } secret, err := h.generateSecret(service) assert.NoError(t, err) assert.Nil(t, secret) } func TestHandler_OnMutationWebhookChange(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockServiceCache := fake.NewMockCacheInterface[*corev1.Service](ctrl) mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) mockMutatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.MutatingWebhookConfiguration, *adminregv1.MutatingWebhookConfigurationList](ctrl) mockService := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-mysecret", nil, []string{"svc.ns", "svc.ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockServiceCache.EXPECT(). Get("ns", "svc"). Return(mockService, nil). Times(2) mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(mockSecret, nil). Times(2) mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).Times(2) mockMutatingWebHooks.EXPECT(). Update(gomock.Any()). DoAndReturn(func(webhook *adminregv1.MutatingWebhookConfiguration) (*adminregv1.MutatingWebhookConfiguration, error) { return webhook, nil }).Times(1) h := &handler{ services: mockServices, serviceCache: mockServiceCache, secretsCache: mockSecretsCache, secrets: mockSecrets, mutatingWebHooks: mockMutatingWebHooks, } webhook := &adminregv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "webhook", }, Webhooks: []adminregv1.MutatingWebhook{ { Name: "wh", ClientConfig: adminregv1.WebhookClientConfig{ Service: &adminregv1.ServiceReference{ Namespace: "ns", Name: "svc", }, CABundle: []byte{}, }, }, { Name: "wh2", ClientConfig: adminregv1.WebhookClientConfig{ Service: &adminregv1.ServiceReference{ Namespace: "ns", Name: "svc", }, CABundle: []byte{}, }, }, }, } updated, err := h.OnMutationWebhookChange("key", webhook) assert.NoError(t, err) assert.NotNil(t, updated) assert.NotEmpty(t, updated.Webhooks[0].ClientConfig.CABundle) assert.NotEmpty(t, updated.Webhooks[1].ClientConfig.CABundle) assert.True(t, bytes.HasPrefix(updated.Webhooks[0].ClientConfig.CABundle, []byte("-----BEGIN CERTIFICATE-----"))) assert.True(t, bytes.HasPrefix(updated.Webhooks[1].ClientConfig.CABundle, []byte("-----BEGIN CERTIFICATE-----"))) } func TestHandler_OnValidatingWebhookChange_Parallel(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() const runs = 10 for i := 0; i < runs; i++ { t.Run(fmt.Sprintf("run-%d", i), func(t *testing.T) { t.Parallel() mockServiceCache := fake.NewMockCacheInterface[*corev1.Service](ctrl) mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) mockValidatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.ValidatingWebhookConfiguration, *adminregv1.ValidatingWebhookConfigurationList](ctrl) mockService := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-mysecret", nil, []string{"svc.ns", "svc.ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockServiceCache.EXPECT(). Get("ns", "svc"). Return(mockService, nil). Times(2) mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(mockSecret, nil). Times(2) mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }). Times(2) mockValidatingWebHooks.EXPECT(). Update(gomock.Any()). DoAndReturn(func(webhook *adminregv1.ValidatingWebhookConfiguration) (*adminregv1.ValidatingWebhookConfiguration, error) { return webhook, nil }).Times(1) h := &handler{ services: mockServices, serviceCache: mockServiceCache, secretsCache: mockSecretsCache, secrets: mockSecrets, validatingWebHooks: mockValidatingWebHooks, } webhook := &adminregv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "webhook", }, Webhooks: []adminregv1.ValidatingWebhook{ { Name: "wh", ClientConfig: adminregv1.WebhookClientConfig{ Service: &adminregv1.ServiceReference{ Namespace: "ns", Name: "svc", }, CABundle: []byte{}, }, }, { Name: "wh2", ClientConfig: adminregv1.WebhookClientConfig{ Service: &adminregv1.ServiceReference{ Namespace: "ns", Name: "svc", }, CABundle: []byte{}, }, }, }, } updated, err := h.OnValidatingWebhookChange("key", webhook) assert.NoError(t, err) assert.NotNil(t, updated) assert.NotEmpty(t, updated.Webhooks[0].ClientConfig.CABundle) assert.NotEmpty(t, updated.Webhooks[1].ClientConfig.CABundle) assert.True(t, bytes.HasPrefix(updated.Webhooks[0].ClientConfig.CABundle, []byte("-----BEGIN CERTIFICATE-----"))) assert.True(t, bytes.HasPrefix(updated.Webhooks[1].ClientConfig.CABundle, []byte("-----BEGIN CERTIFICATE-----"))) }) } } func TestHandler_OnMutationWebhookChange_Parallel(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() const runs = 10 for i := 0; i < runs; i++ { t.Run(fmt.Sprintf("run-%d", i), func(t *testing.T) { t.Parallel() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockServiceCache := fake.NewMockCacheInterface[*corev1.Service](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) mockMutatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.MutatingWebhookConfiguration, *adminregv1.MutatingWebhookConfigurationList](ctrl) mockService := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-mysecret", nil, []string{"svc.ns", "svc.ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockServiceCache.EXPECT(). Get("ns", "svc"). Return(mockService, nil) mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(mockSecret, nil) mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }) mockMutatingWebHooks.EXPECT(). Update(gomock.Any()). DoAndReturn(func(webhook *adminregv1.MutatingWebhookConfiguration) (*adminregv1.MutatingWebhookConfiguration, error) { return webhook, nil }) h := &handler{ services: mockServices, serviceCache: mockServiceCache, secretsCache: mockSecretsCache, secrets: mockSecrets, mutatingWebHooks: mockMutatingWebHooks, } webhook := &adminregv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "webhook", }, Webhooks: []adminregv1.MutatingWebhook{ { Name: "wh", ClientConfig: adminregv1.WebhookClientConfig{ Service: &adminregv1.ServiceReference{ Namespace: "ns", Name: "svc", }, CABundle: []byte{}, }, }, }, } updated, err := h.OnMutationWebhookChange("key", webhook) assert.NoError(t, err) assert.NotNil(t, updated) assert.NotEmpty(t, updated.Webhooks[0].ClientConfig.CABundle) assert.True(t, bytes.HasPrefix(updated.Webhooks[0].ClientConfig.CABundle, []byte("-----BEGIN CERTIFICATE-----"))) }) } } func TestHandler_OnService_Parallel(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() const runs = 10 for i := 0; i < runs; i++ { t.Run(fmt.Sprintf("run-%d", i), func(t *testing.T) { t.Parallel() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockServiceCache := fake.NewMockCacheInterface[*corev1.Service](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) mockMutatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.MutatingWebhookConfiguration, *adminregv1.MutatingWebhookConfigurationList](ctrl) mockValidatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.ValidatingWebhookConfiguration, *adminregv1.ValidatingWebhookConfigurationList](ctrl) mockCRDs := fake.NewMockNonNamespacedControllerInterface[*apiextv1.CustomResourceDefinition, *apiextv1.CustomResourceDefinitionList](ctrl) mockMutatingCache := fake.NewMockNonNamespacedCacheInterface[*adminregv1.MutatingWebhookConfiguration](ctrl) mockValidatingCache := fake.NewMockNonNamespacedCacheInterface[*adminregv1.ValidatingWebhookConfiguration](ctrl) mockCRDsCache := fake.NewMockNonNamespacedCacheInterface[*apiextv1.CustomResourceDefinition](ctrl) mockMutatingWebHooks.EXPECT().Cache().Return(mockMutatingCache).AnyTimes() mockValidatingWebHooks.EXPECT().Cache().Return(mockValidatingCache).AnyTimes() mockCRDs.EXPECT().Cache().Return(mockCRDsCache).AnyTimes() mockMutatingCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return([]*adminregv1.MutatingWebhookConfiguration{}, nil).AnyTimes() mockValidatingCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return([]*adminregv1.ValidatingWebhookConfiguration{}, nil).AnyTimes() mockCRDsCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return([]*apiextv1.CustomResourceDefinition{}, nil).AnyTimes() mockMutatingWebHooks.EXPECT().Enqueue(gomock.Any()).AnyTimes() mockValidatingWebHooks.EXPECT().Enqueue(gomock.Any()).AnyTimes() mockCRDs.EXPECT().Enqueue(gomock.Any()).AnyTimes() service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-mysecret", nil, []string{"svc.ns", "svc.ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(mockSecret, nil).AnyTimes() mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() h := &handler{ services: mockServices, serviceCache: mockServiceCache, secretsCache: mockSecretsCache, secrets: mockSecrets, mutatingWebHooks: mockMutatingWebHooks, validatingWebHooks: mockValidatingWebHooks, crds: mockCRDs, } _, err := h.OnService("ns/svc", service) assert.NoError(t, err) }) } } func TestHandler_OnCRDChange_Parallel(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() const runs = 10 for i := 0; i < runs; i++ { t.Run(fmt.Sprintf("run-%d", i), func(t *testing.T) { t.Parallel() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockServiceCache := fake.NewMockCacheInterface[*corev1.Service](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) mockCRDs := fake.NewMockNonNamespacedControllerInterface[*apiextv1.CustomResourceDefinition, *apiextv1.CustomResourceDefinitionList](ctrl) mockCRDsCache := fake.NewMockNonNamespacedCacheInterface[*apiextv1.CustomResourceDefinition](ctrl) mockCRDs.EXPECT().Cache().Return(mockCRDsCache).AnyTimes() mockCRDs.EXPECT().Enqueue(gomock.Any()).AnyTimes() service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-mysecret", nil, []string{"svc.ns", "svc.ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockServiceCache.EXPECT(). Get("ns", "svc"). Return(service, nil).AnyTimes() mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(mockSecret, nil).AnyTimes() mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() mockSecrets.EXPECT(). Create(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() mockCRDs.EXPECT(). Update(gomock.Any()). DoAndReturn(func(crd *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, error) { return crd, nil }).AnyTimes() h := &handler{ services: mockServices, serviceCache: mockServiceCache, secretsCache: mockSecretsCache, secrets: mockSecrets, crds: mockCRDs, } crd := &apiextv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "crd", }, Spec: apiextv1.CustomResourceDefinitionSpec{ Conversion: &apiextv1.CustomResourceConversion{ Strategy: apiextv1.WebhookConverter, Webhook: &apiextv1.WebhookConversion{ ClientConfig: &apiextv1.WebhookClientConfig{ Service: &apiextv1.ServiceReference{ Namespace: "ns", Name: "svc", }, CABundle: []byte{}, }, }, }, }, } updated, err := h.OnCRDChange("key", crd) assert.NoError(t, err) assert.NotNil(t, updated) assert.NotEmpty(t, updated.Spec.Conversion.Webhook.ClientConfig.CABundle) assert.True(t, bytes.HasPrefix(updated.Spec.Conversion.Webhook.ClientConfig.CABundle, []byte("-----BEGIN CERTIFICATE-----"))) }) } } func TestHandler_GenerateSecret_Race(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-mysecret", nil, []string{"svc.ns", "svc.ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(mockSecret, nil).AnyTimes() mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() h := &handler{ services: mockServices, secretsCache: mockSecretsCache, secrets: mockSecrets, } const concurrency = 10 done := make(chan struct{}) for i := 0; i < concurrency; i++ { go func() { _, err := h.generateSecret(service) assert.NoError(t, err) done <- struct{}{} }() } for i := 0; i < concurrency; i++ { <-done } } func TestHandler_GenerateSecret_Race_MultiService(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) const concurrency = 10 done := make(chan struct{}) for i := 0; i < concurrency; i++ { serviceName := fmt.Sprintf("svc-%d", i) secretName := fmt.Sprintf("secret-%d", i) service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: serviceName, Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: secretName, }, }, } certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-"+secretName, nil, []string{serviceName + ".ns", serviceName + ".ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockServices.EXPECT(). EnqueueAfter("ns", gomock.Any(), gomock.Any()). AnyTimes() mockSecretsCache.EXPECT(). Get("ns", secretName). Return(mockSecret, nil).AnyTimes() mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() go func(svc *corev1.Service) { h := &handler{ services: mockServices, secretsCache: mockSecretsCache, secrets: mockSecrets, } _, err := h.generateSecret(svc) assert.NoError(t, err) done <- struct{}{} }(service) } for i := 0; i < concurrency; i++ { <-done } } func TestHandler_Race_Stress(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockServiceCache := fake.NewMockCacheInterface[*corev1.Service](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) mockMutatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.MutatingWebhookConfiguration, *adminregv1.MutatingWebhookConfigurationList](ctrl) mockValidatingWebHooks := fake.NewMockNonNamespacedControllerInterface[*adminregv1.ValidatingWebhookConfiguration, *adminregv1.ValidatingWebhookConfigurationList](ctrl) mockCRDs := fake.NewMockNonNamespacedControllerInterface[*apiextv1.CustomResourceDefinition, *apiextv1.CustomResourceDefinitionList](ctrl) mockMutatingCache := fake.NewMockNonNamespacedCacheInterface[*adminregv1.MutatingWebhookConfiguration](ctrl) mockValidatingCache := fake.NewMockNonNamespacedCacheInterface[*adminregv1.ValidatingWebhookConfiguration](ctrl) mockCRDsCache := fake.NewMockNonNamespacedCacheInterface[*apiextv1.CustomResourceDefinition](ctrl) mockMutatingWebHooks.EXPECT().Cache().Return(mockMutatingCache).AnyTimes() mockValidatingWebHooks.EXPECT().Cache().Return(mockValidatingCache).AnyTimes() mockCRDs.EXPECT().Cache().Return(mockCRDsCache).AnyTimes() mockMutatingCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return([]*adminregv1.MutatingWebhookConfiguration{}, nil).AnyTimes() mockValidatingCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return([]*adminregv1.ValidatingWebhookConfiguration{}, nil).AnyTimes() mockCRDsCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return([]*apiextv1.CustomResourceDefinition{}, nil).AnyTimes() mockMutatingWebHooks.EXPECT().Enqueue(gomock.Any()).AnyTimes() mockValidatingWebHooks.EXPECT().Enqueue(gomock.Any()).AnyTimes() mockCRDs.EXPECT().Enqueue(gomock.Any()).AnyTimes() mockSecrets.EXPECT().Update(gomock.Any()).DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() mockSecrets.EXPECT().Create(gomock.Any()).DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() mockCRDs.EXPECT().Update(gomock.Any()).DoAndReturn(func(crd *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, error) { return crd, nil }).AnyTimes() h := &handler{ services: mockServices, serviceCache: mockServiceCache, secretsCache: mockSecretsCache, secrets: mockSecrets, mutatingWebHooks: mockMutatingWebHooks, validatingWebHooks: mockValidatingWebHooks, crds: mockCRDs, } const concurrency = 10 done := make(chan struct{}) for i := 0; i < concurrency; i++ { go func(i int) { serviceName := fmt.Sprintf("svc-%d", i%5) secretName := fmt.Sprintf("secret-%d", i%5) service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: serviceName, Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: secretName, }, }, } mockServices.EXPECT(). EnqueueAfter("ns", gomock.Any(), gomock.Any()). AnyTimes() mockServiceCache.EXPECT(). Get("ns", serviceName). Return(service, nil).AnyTimes() certPEM, keyPEM, _ := cert.GenerateSelfSignedCertKey("ns-"+secretName, nil, []string{serviceName + ".ns", serviceName + ".ns.svc"}) mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockSecretsCache.EXPECT(). Get("ns", secretName). Return(mockSecret, nil).AnyTimes() switch i % 3 { case 0: _, err := h.generateSecret(service) assert.NoError(t, err) case 1: _, err := h.OnService("ns/"+serviceName, service) assert.NoError(t, err) case 2: crd := &apiextv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "crd-" + serviceName, }, Spec: apiextv1.CustomResourceDefinitionSpec{ Conversion: &apiextv1.CustomResourceConversion{ Strategy: apiextv1.WebhookConverter, Webhook: &apiextv1.WebhookConversion{ ClientConfig: &apiextv1.WebhookClientConfig{ Service: &apiextv1.ServiceReference{ Namespace: "ns", Name: serviceName, }, CABundle: []byte{}, }, }, }, }, } _, err := h.OnCRDChange("key", crd) assert.NoError(t, err) } done <- struct{}{} }(i) } for i := 0; i < concurrency; i++ { <-done } } func TestHandler_ParseCert_CorruptedData(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "badsecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: []byte("-----BEGIN CERTIFICATE-----\nMIIB fake cert\n-----END CERTIFICATE-----"), corev1.TLSPrivateKeyKey: []byte("not-a-key"), }, } parsed, err := parseCert(secret) assert.Error(t, err, "expected error when parsing corrupted TLS secret") assert.Nil(t, parsed, "no updated secret should be returned on corrupted data") } func TestHandler_GenerateSecret_Race_SharedSecret(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) // Both services point to the same secret name service1 := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc1", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "shared-secret", }, }, } service2 := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc2", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "shared-secret", }, }, } // Intentionally returning notfound from the cache each time so that // multiple goroutines will attempt to create the same secret concurrently. mockServices.EXPECT(). EnqueueAfter("ns", gomock.Any(), gomock.Any()). AnyTimes() mockSecretsCache.EXPECT(). Get("ns", "shared-secret"). Return(nil, apierror.NewNotFound(corev1.Resource("secrets"), "shared-secret")). AnyTimes() mockSecrets.EXPECT(). Create(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() mockSecrets.EXPECT(). Update(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return secret, nil }).AnyTimes() h := &handler{ services: mockServices, secretsCache: mockSecretsCache, secrets: mockSecrets, } const concurrency = 10 done := make(chan struct{}) for i := 0; i < concurrency; i++ { go func(i int) { var svc *corev1.Service if i%2 == 0 { svc = service1 } else { svc = service2 } _, err := h.generateSecret(svc) assert.NoError(t, err) done <- struct{}{} }(i) } for i := 0; i < concurrency; i++ { <-done } } func TestHandler_GenerateSecret_StaleCacheAlreadyExists(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockSecretsCache := fake.NewMockCacheInterface[*corev1.Secret](ctrl) mockSecrets := fake.NewMockControllerInterface[*corev1.Secret, *corev1.SecretList](ctrl) service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc", Namespace: "ns", Annotations: map[string]string{ SecretAnnotation: "mysecret", }, }, } // Simulate cache always lags and reports NotFound mockServices.EXPECT(). EnqueueAfter("ns", "svc", gomock.Any()). AnyTimes() mockSecretsCache.EXPECT(). Get("ns", "mysecret"). Return(nil, apierror.NewNotFound(corev1.Resource("secrets"), "mysecret")). AnyTimes() mockSecrets.EXPECT(). Create(gomock.Any()). DoAndReturn(func(secret *corev1.Secret) (*corev1.Secret, error) { return nil, apierror.NewAlreadyExists(corev1.Resource("secrets"), "mysecret") }). AnyTimes() certPEM, keyPEM, err := cert.GenerateSelfSignedCertKey("mysecret", nil, []string{"svc.ns"}) assert.NoError(t, err) expectedSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } mockSecrets.EXPECT(). Get("ns", "mysecret", gomock.Any()). Return(expectedSecret, nil). AnyTimes() mockSecrets.EXPECT(). Update(gomock.Any()). Return(expectedSecret, nil). AnyTimes() h := &handler{ services: mockServices, secretsCache: mockSecretsCache, secrets: mockSecrets, } secret, err := h.generateSecret(service) assert.NoError(t, err) assert.NotNil(t, secret) } func TestHandler_scheduleNextCertCheck(t *testing.T) { type enqueueCall struct { ns, name string delay time.Duration } var calls []enqueueCall ctrl := gomock.NewController(t) defer ctrl.Finish() mockServices := fake.NewMockControllerInterface[*corev1.Service, *corev1.ServiceList](ctrl) mockServices.EXPECT(). EnqueueAfter(gomock.Any(), gomock.Any(), gomock.Any()). Do(func(ns, name string, delay time.Duration) { calls = append(calls, enqueueCall{ns, name, delay}) }). Times(2) h := &handler{services: mockServices} tests := []struct { name string maxAge time.Duration wantDelay time.Duration wantErr bool }{ { name: "cert expires in 90 days → schedule at 30 days", maxAge: 90 * 24 * time.Hour, wantDelay: 30 * 24 * time.Hour, }, { name: "cert expires in 30 days → clamp to 1 minute", maxAge: 30 * 24 * time.Hour, wantDelay: time.Minute, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { calls = nil certPEM, keyPEM, err := cert.GenerateSelfSignedCertKeyWithOptions(cert.SelfSignedCertKeyOptions{ Host: "ns-mysecret", MaxAge: tt.maxAge, }) assert.NoError(t, err) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "ns", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM, }, } obj := &corev1.Service{} err = h.scheduleNextCertCheck(obj, secret) if (err != nil) != tt.wantErr { t.Fatalf("scheduleNextCertCheck() error = %v, wantErr %v", err, tt.wantErr) } if len(calls) != 1 { t.Fatalf("expected 1 EnqueueAfter call, got %d", len(calls)) } got := calls[0].delay tolerance := 1*time.Hour + 10*time.Second if math.Abs(got.Seconds()-tt.wantDelay.Seconds()) > tolerance.Seconds() { t.Errorf("expected delay ~%v, got %v", tt.wantDelay, got) } }) } } ================================================ FILE: pkg/objectset/objectset.go ================================================ package objectset import ( "fmt" "reflect" "sort" "github.com/rancher/wrangler/v3/pkg/gvk" "github.com/rancher/wrangler/v3/pkg/stringset" "github.com/rancher/wrangler/v3/pkg/merr" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) type ObjectKey struct { Name string Namespace string } func NewObjectKey(obj v1.Object) ObjectKey { return ObjectKey{ Namespace: obj.GetNamespace(), Name: obj.GetName(), } } func (o ObjectKey) String() string { if o.Namespace == "" { return o.Name } return fmt.Sprintf("%s/%s", o.Namespace, o.Name) } type ObjectKeyByGVK map[schema.GroupVersionKind][]ObjectKey type ObjectByGVK map[schema.GroupVersionKind]map[ObjectKey]runtime.Object func (o ObjectByGVK) Add(obj runtime.Object) (schema.GroupVersionKind, error) { metadata, err := meta.Accessor(obj) if err != nil { return schema.GroupVersionKind{}, err } gvk, err := gvk.Get(obj) if err != nil { return schema.GroupVersionKind{}, err } objs := o[gvk] if objs == nil { objs = ObjectByKey{} o[gvk] = objs } objs[ObjectKey{ Namespace: metadata.GetNamespace(), Name: metadata.GetName(), }] = obj return gvk, nil } type ObjectSet struct { errs []error objects ObjectByGVK objectsByGK ObjectByGK order []runtime.Object gvkOrder []schema.GroupVersionKind gvkSeen map[schema.GroupVersionKind]bool } func NewObjectSet(objs ...runtime.Object) *ObjectSet { os := &ObjectSet{ objects: ObjectByGVK{}, objectsByGK: ObjectByGK{}, gvkSeen: map[schema.GroupVersionKind]bool{}, } os.Add(objs...) return os } func (o *ObjectSet) ObjectsByGVK() ObjectByGVK { if o == nil { return nil } return o.objects } func (o *ObjectSet) Contains(gk schema.GroupKind, key ObjectKey) bool { _, ok := o.objectsByGK[gk][key] return ok } func (o *ObjectSet) All() []runtime.Object { return o.order } func (o *ObjectSet) Add(objs ...runtime.Object) *ObjectSet { for _, obj := range objs { o.add(obj) } return o } func (o *ObjectSet) add(obj runtime.Object) { if obj == nil || reflect.ValueOf(obj).IsNil() { return } gvk, err := o.objects.Add(obj) if err != nil { o.err(fmt.Errorf("failed to add %T: %w", obj, err)) return } _, err = o.objectsByGK.Add(obj) if err != nil { o.err(fmt.Errorf("failed to add %T: %w", obj, err)) return } o.order = append(o.order, obj) if !o.gvkSeen[gvk] { o.gvkSeen[gvk] = true o.gvkOrder = append(o.gvkOrder, gvk) } } func (o *ObjectSet) err(err error) error { o.errs = append(o.errs, err) return o.Err() } func (o *ObjectSet) AddErr(err error) { o.errs = append(o.errs, err) } func (o *ObjectSet) Err() error { return merr.NewErrors(o.errs...) } func (o *ObjectSet) Len() int { return len(o.objects) } func (o *ObjectSet) GVKs() []schema.GroupVersionKind { return o.GVKOrder() } func (o *ObjectSet) GVKOrder(known ...schema.GroupVersionKind) []schema.GroupVersionKind { var rest []schema.GroupVersionKind for _, gvk := range known { if o.gvkSeen[gvk] { continue } rest = append(rest, gvk) } sort.Slice(rest, func(i, j int) bool { return rest[i].String() < rest[j].String() }) return append(o.gvkOrder, rest...) } // Namespaces all distinct namespaces found on the objects in this set. func (o *ObjectSet) Namespaces() []string { namespaces := stringset.Set{} for _, objsByKey := range o.ObjectsByGVK() { for objKey := range objsByKey { namespaces.Add(objKey.Namespace) } } return namespaces.Values() } type ObjectByKey map[ObjectKey]runtime.Object func (o ObjectByKey) Namespaces() []string { namespaces := stringset.Set{} for objKey := range o { namespaces.Add(objKey.Namespace) } return namespaces.Values() } type ObjectByGK map[schema.GroupKind]map[ObjectKey]runtime.Object func (o ObjectByGK) Add(obj runtime.Object) (schema.GroupKind, error) { metadata, err := meta.Accessor(obj) if err != nil { return schema.GroupKind{}, err } gvk, err := gvk.Get(obj) if err != nil { return schema.GroupKind{}, err } gk := gvk.GroupKind() objs := o[gk] if objs == nil { objs = ObjectByKey{} o[gk] = objs } objs[ObjectKey{ Namespace: metadata.GetNamespace(), Name: metadata.GetName(), }] = obj return gk, nil } ================================================ FILE: pkg/objectset/objectset_test.go ================================================ package objectset import ( "testing" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) func TestObjectSet_Namespaces(t *testing.T) { type fields struct { errs []error objects ObjectByGVK objectsByGK ObjectByGK order []runtime.Object gvkOrder []schema.GroupVersionKind gvkSeen map[schema.GroupVersionKind]bool } tests := []struct { name string fields fields wantNamespaces []string }{ { name: "empty", fields: fields{ objects: map[schema.GroupVersionKind]map[ObjectKey]runtime.Object{}, }, wantNamespaces: nil, }, { name: "1 namespace", fields: fields{ objects: map[schema.GroupVersionKind]map[ObjectKey]runtime.Object{ schema.GroupVersionKind{}: { ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Namespace: "ns1", Name: "b"}: nil, }, }, }, wantNamespaces: []string{"ns1"}, }, { name: "many namespace", fields: fields{ objects: map[schema.GroupVersionKind]map[ObjectKey]runtime.Object{ schema.GroupVersionKind{}: { ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Namespace: "ns2", Name: "b"}: nil, }, }, }, wantNamespaces: []string{"ns1", "ns2"}, }, { name: "missing namespace", fields: fields{ objects: map[schema.GroupVersionKind]map[ObjectKey]runtime.Object{ schema.GroupVersionKind{}: { ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Name: "b"}: nil, }, }, }, wantNamespaces: []string{"", "ns1"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := &ObjectSet{ errs: tt.fields.errs, objects: tt.fields.objects, objectsByGK: tt.fields.objectsByGK, order: tt.fields.order, gvkOrder: tt.fields.gvkOrder, gvkSeen: tt.fields.gvkSeen, } gotNamespaces := o.Namespaces() assert.ElementsMatchf(t, tt.wantNamespaces, gotNamespaces, "Namespaces() = %v, want %v", gotNamespaces, tt.wantNamespaces) }) } } func TestObjectByKey_Namespaces(t *testing.T) { tests := []struct { name string objects ObjectByKey wantNamespaces []string }{ { name: "empty", objects: ObjectByKey{}, wantNamespaces: nil, }, { name: "1 namespace", objects: ObjectByKey{ ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Namespace: "ns1", Name: "b"}: nil, }, wantNamespaces: []string{"ns1"}, }, { name: "many namespaces", objects: ObjectByKey{ ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Namespace: "ns2", Name: "b"}: nil, }, wantNamespaces: []string{"ns1", "ns2"}, }, { name: "many namespaces with duplicates", objects: ObjectByKey{ ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Namespace: "ns2", Name: "b"}: nil, ObjectKey{Namespace: "ns1", Name: "c"}: nil, }, wantNamespaces: []string{"ns1", "ns2"}, }, { name: "missing namespace", objects: ObjectByKey{ ObjectKey{Namespace: "ns1", Name: "a"}: nil, ObjectKey{Name: "b"}: nil, }, wantNamespaces: []string{"", "ns1"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotNamespaces := tt.objects.Namespaces() assert.ElementsMatchf(t, tt.wantNamespaces, gotNamespaces, "Namespaces() = %v, want %v", gotNamespaces, tt.wantNamespaces) }) } } ================================================ FILE: pkg/patch/apply.go ================================================ package patch import ( "encoding/json" "fmt" jsonpatch "github.com/evanphx/json-patch" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" ) func Apply(original, patch []byte) ([]byte, error) { style, metadata, err := GetPatchStyle(original, patch) if err != nil { return nil, err } switch style { case types.JSONPatchType: return applyJSONPatch(original, patch) case types.MergePatchType: return applyMergePatch(original, patch) case types.StrategicMergePatchType: return applyStrategicMergePatch(original, patch, metadata) default: return nil, fmt.Errorf("invalid patch") } } func applyStrategicMergePatch(original, patch []byte, lookup strategicpatch.LookupPatchMeta) ([]byte, error) { originalMap := map[string]interface{}{} patchMap := map[string]interface{}{} if err := json.Unmarshal(original, &originalMap); err != nil { return nil, err } if err := json.Unmarshal(patch, &patchMap); err != nil { return nil, err } patchedMap, err := strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(originalMap, patchMap, lookup) if err != nil { return nil, err } return json.Marshal(patchedMap) } func applyMergePatch(original, patch []byte) ([]byte, error) { return jsonpatch.MergePatch(original, patch) } func applyJSONPatch(original, patch []byte) ([]byte, error) { jsonPatch, err := jsonpatch.DecodePatch(patch) if err != nil { return nil, err } return jsonPatch.Apply(original) } ================================================ FILE: pkg/patch/style.go ================================================ package patch import ( "sync" "github.com/rancher/wrangler/v3/pkg/gvk" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes/scheme" ) var ( patchCache = map[schema.GroupVersionKind]patchCacheEntry{} patchCacheLock = sync.Mutex{} ) type patchCacheEntry struct { patchType types.PatchType lookup strategicpatch.LookupPatchMeta } func isJSONPatch(patch []byte) bool { // a JSON patch is a list return len(patch) > 0 && patch[0] == '[' } func GetPatchStyle(original, patch []byte) (types.PatchType, strategicpatch.LookupPatchMeta, error) { if isJSONPatch(patch) { return types.JSONPatchType, nil, nil } gvk, ok, err := gvk.Detect(original) if err != nil { return "", nil, err } if !ok { return types.MergePatchType, nil, nil } return GetMergeStyle(gvk) } func GetMergeStyle(gvk schema.GroupVersionKind) (types.PatchType, strategicpatch.LookupPatchMeta, error) { var ( patchType types.PatchType lookupPatchMeta strategicpatch.LookupPatchMeta ) patchCacheLock.Lock() entry, ok := patchCache[gvk] patchCacheLock.Unlock() if ok { return entry.patchType, entry.lookup, nil } versionedObject, err := scheme.Scheme.New(gvk) if runtime.IsNotRegisteredError(err) || gvk.Kind == "CustomResourceDefinition" { patchType = types.MergePatchType } else if err != nil { return patchType, nil, err } else { patchType = types.StrategicMergePatchType lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject) if err != nil { return patchType, nil, err } } patchCacheLock.Lock() patchCache[gvk] = patchCacheEntry{ patchType: patchType, lookup: lookupPatchMeta, } patchCacheLock.Unlock() return patchType, lookupPatchMeta, nil } ================================================ FILE: pkg/randomtoken/token.go ================================================ package randomtoken import ( "crypto/rand" "math/big" ) const ( characters = "bcdfghjklmnpqrstvwxz2456789" tokenLength = 54 ) var charsLength = big.NewInt(int64(len(characters))) func Generate() (string, error) { token := make([]byte, tokenLength) for i := range token { r, err := rand.Int(rand.Reader, charsLength) if err != nil { return "", err } token[i] = characters[r.Int64()] } return string(token), nil } ================================================ FILE: pkg/ratelimit/none.go ================================================ package ratelimit import ( "context" "k8s.io/client-go/util/flowcontrol" ) var ( None = flowcontrol.RateLimiter((*none)(nil)) ) type none struct{} func (*none) TryAccept() bool { return true } func (*none) Stop() {} func (*none) Accept() {} func (*none) QPS() float32 { return 1 } func (*none) Wait(_ context.Context) error { return nil } ================================================ FILE: pkg/relatedresource/all.go ================================================ package relatedresource import "k8s.io/apimachinery/pkg/runtime" const ( AllKey = "_all_" ) func TriggerAllKey(namespace, name string, obj runtime.Object) ([]Key, error) { if name != AllKey { return []Key{{ Name: AllKey, }}, nil } return nil, nil } ================================================ FILE: pkg/relatedresource/changeset.go ================================================ package relatedresource import ( "context" "time" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/meta" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/kv" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" ) type Key struct { Namespace string Name string } func NewKey(namespace, name string) Key { return Key{ Namespace: namespace, Name: name, } } func FromString(key string) Key { return NewKey(kv.RSplit(key, "/")) } type ControllerWrapper interface { Informer() cache.SharedIndexInformer AddGenericHandler(ctx context.Context, name string, handler generic.Handler) } type ClusterScopedEnqueuer interface { Enqueue(name string) } type Enqueuer interface { Enqueue(namespace, name string) } type Resolver func(namespace, name string, obj runtime.Object) ([]Key, error) func WatchClusterScoped(ctx context.Context, name string, resolve Resolver, enq ClusterScopedEnqueuer, watching ...ControllerWrapper) { Watch(ctx, name, resolve, &wrapper{ClusterScopedEnqueuer: enq}, watching...) } func Watch(ctx context.Context, name string, resolve Resolver, enq Enqueuer, watching ...ControllerWrapper) { for _, c := range watching { watch(ctx, name, enq, resolve, c) } } func watch(ctx context.Context, name string, enq Enqueuer, resolve Resolver, controller ControllerWrapper) { runResolve := func(ns, name string, obj runtime.Object) error { keys, err := resolve(ns, name, obj) if err != nil { return err } for _, key := range keys { if key.Name != "" { enq.Enqueue(key.Namespace, key.Name) } } return nil } addResourceEventHandler(ctx, controller.Informer(), cache.ResourceEventHandlerFuncs{ DeleteFunc: func(obj interface{}) { ro, ok := obj.(runtime.Object) if !ok { return } meta, err := meta.Accessor(ro) if err != nil { return } go func() { time.Sleep(time.Second) runResolve(meta.GetNamespace(), meta.GetName(), ro) }() }, }) controller.AddGenericHandler(ctx, name, func(key string, obj runtime.Object) (runtime.Object, error) { ns, name := kv.RSplit(key, "/") return obj, runResolve(ns, name, obj) }) } type wrapper struct { ClusterScopedEnqueuer } func (w *wrapper) Enqueue(namespace, name string) { w.ClusterScopedEnqueuer.Enqueue(name) } // informerRegisterer is a subset of the cache.SharedIndexInformer, so it's easier to replace in tests type informerRegisterer interface { AddEventHandler(funcs cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) RemoveEventHandler(cache.ResourceEventHandlerRegistration) error } func addResourceEventHandler(ctx context.Context, informer informerRegisterer, handler cache.ResourceEventHandler) { handlerReg, err := informer.AddEventHandler(handler) if err != nil { logrus.WithError(err).Error("failed to add ResourceEventHandler") return } go func() { <-ctx.Done() if err := informer.RemoveEventHandler(handlerReg); err != nil { logrus.WithError(err).Warn("failed to remove ResourceEventHandler") } }() } ================================================ FILE: pkg/relatedresource/changeset_test.go ================================================ package relatedresource import ( "context" "fmt" "testing" "time" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" ) func Test_addResourceEventHandler(t *testing.T) { const expectedCalls = 5 ctx, cancel := context.WithCancel(context.Background()) defer cancel() var counter int handler := &cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { counter++ }, } informer := &fakeInformer{} addResourceEventHandler(ctx, informer, handler) for i := 0; i < expectedCalls; i++ { informer.add(nil) } if want, got := expectedCalls, counter; want != got { t.Errorf("unexpected number of executions of handler func, want: %d, got: %d", want, got) } // Close enqueuer context and wait for unregistering goroutine cancel() informer.waitUntilDeleted(handler, 1*time.Second) // New informer calls should not trigger our handler informer.add(nil) if got, want := counter, expectedCalls; got != want { t.Errorf("resource event handler is not correctly removed") } } type handlerRegistration struct{} func (h handlerRegistration) HasSynced() bool { return true } // fakeInformer implements a subset of cache.SharedIndexInformer, only those methods used by addResourceEventHandler type fakeInformer struct { handlers []cache.ResourceEventHandler reg []cache.ResourceEventHandlerRegistration deleteChan []chan struct{} } func (informer *fakeInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) { handlerReg := handlerRegistration{} informer.handlers = append(informer.handlers, handler) informer.reg = append(informer.reg, handlerReg) informer.deleteChan = append(informer.deleteChan, make(chan struct{})) return handlerReg, nil } func (informer *fakeInformer) RemoveEventHandler(handlerReg cache.ResourceEventHandlerRegistration) error { x := slicesIndex(informer.reg, handlerReg) if x < 0 { return fmt.Errorf("handler not found") } close(informer.deleteChan[x]) informer.reg = deleteIndex(informer.reg, x) informer.handlers = deleteIndex(informer.handlers, x) informer.deleteChan = deleteIndex(informer.deleteChan, x) return nil } func (informer *fakeInformer) add(obj runtime.Object) { for _, handler := range informer.handlers { handler.OnAdd(obj, false) } } func (informer *fakeInformer) waitUntilDeleted(handler cache.ResourceEventHandler, timeout time.Duration) { x := slicesIndex(informer.handlers, handler) if x < 0 { return } select { case <-informer.deleteChan[x]: case <-time.After(timeout): } } func slicesIndex[S ~[]E, E comparable](s S, v E) int { for i := range s { if v == s[i] { return i } } return -1 } func deleteIndex[S ~[]E, E any](s S, i int) S { return append(s[:i], s[i+1:]...) } ================================================ FILE: pkg/relatedresource/owner.go ================================================ package relatedresource import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" ) // OwnerResolver Look for owner references that match the apiVersion and kind and resolve to the namespace and // name of the parent. The namespaced flag is whether the apiVersion/kind referenced is expected to be namespaced func OwnerResolver(namespaced bool, apiVersion, kind string) Resolver { return func(namespace, name string, obj runtime.Object) ([]Key, error) { if obj == nil { return nil, nil } meta, err := meta.Accessor(obj) if err != nil { // ignore err return nil, nil } var result []Key for _, owner := range meta.GetOwnerReferences() { if owner.Kind == kind && owner.APIVersion == apiVersion { ns := "" if namespaced { ns = meta.GetNamespace() } result = append(result, Key{ Namespace: ns, Name: owner.Name, }) } } return result, nil } } ================================================ FILE: pkg/resolvehome/main.go ================================================ package resolvehome import ( "fmt" "os" "strings" ) var ( homes = []string{"$HOME", "${HOME}", "~"} ) func Resolve(s string) (string, error) { for _, home := range homes { if strings.Contains(s, home) { homeDir, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("determining current user: %w", err) } s = strings.Replace(s, home, homeDir, -1) } } return s, nil } ================================================ FILE: pkg/schemas/definition/definition.go ================================================ package definition import ( "strings" "github.com/rancher/wrangler/v3/pkg/data/convert" ) func IsMapType(fieldType string) bool { return strings.HasPrefix(fieldType, "map[") && strings.HasSuffix(fieldType, "]") } func IsArrayType(fieldType string) bool { return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]") } func IsReferenceType(fieldType string) bool { return strings.HasPrefix(fieldType, "reference[") && strings.HasSuffix(fieldType, "]") } func HasReferenceType(fieldType string) bool { return strings.Contains(fieldType, "reference[") } func SubType(fieldType string) string { i := strings.Index(fieldType, "[") if i <= 0 || i >= len(fieldType)-1 { return fieldType } return fieldType[i+1 : len(fieldType)-1] } func GetType(data map[string]interface{}) string { return convert.ToString(data["type"]) } ================================================ FILE: pkg/schemas/mapper.go ================================================ package schemas import ( "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/merr" "github.com/rancher/wrangler/v3/pkg/schemas/definition" ) type Mapper interface { FromInternal(data data.Object) ToInternal(data data.Object) error ModifySchema(schema *Schema, schemas *Schemas) error } type Mappers []Mapper func (m Mappers) FromInternal(data data.Object) { for _, mapper := range m { mapper.FromInternal(data) } } func (m Mappers) ToInternal(data data.Object) error { var errors []error for i := len(m) - 1; i >= 0; i-- { errors = append(errors, m[i].ToInternal(data)) } return merr.NewErrors(errors...) } func (m Mappers) ModifySchema(schema *Schema, schemas *Schemas) error { for _, mapper := range m { if err := mapper.ModifySchema(schema, schemas); err != nil { return err } } return nil } type typeMapper struct { Mappers []Mapper root bool typeName string subSchemas map[string]*Schema subArraySchemas map[string]*Schema subMapSchemas map[string]*Schema } func (t *typeMapper) FromInternal(data data.Object) { for fieldName, schema := range t.subSchemas { if schema.Mapper == nil { continue } schema.Mapper.FromInternal(data.Map(fieldName)) } for fieldName, schema := range t.subMapSchemas { if schema.Mapper == nil { continue } for _, fieldData := range data.Map(fieldName).Values() { schema.Mapper.FromInternal(fieldData) } } for fieldName, schema := range t.subArraySchemas { if schema.Mapper == nil { continue } for _, fieldData := range data.Slice(fieldName) { schema.Mapper.FromInternal(fieldData) } } Mappers(t.Mappers).FromInternal(data) } func addError(errors []error, err error) []error { if err == nil { return errors } return append(errors, err) } func (t *typeMapper) ToInternal(data data.Object) error { var errors []error errors = addError(errors, Mappers(t.Mappers).ToInternal(data)) for fieldName, schema := range t.subArraySchemas { if schema.Mapper == nil { continue } for _, fieldData := range data.Slice(fieldName) { errors = addError(errors, schema.Mapper.ToInternal(fieldData)) } } for fieldName, schema := range t.subMapSchemas { if schema.Mapper == nil { continue } for _, fieldData := range data.Map(fieldName) { errors = addError(errors, schema.Mapper.ToInternal(convert.ToMapInterface(fieldData))) } } for fieldName, schema := range t.subSchemas { if schema.Mapper == nil { continue } errors = addError(errors, schema.Mapper.ToInternal(data.Map(fieldName))) } return merr.NewErrors(errors...) } func (t *typeMapper) ModifySchema(schema *Schema, schemas *Schemas) error { t.subSchemas = map[string]*Schema{} t.subArraySchemas = map[string]*Schema{} t.subMapSchemas = map[string]*Schema{} t.typeName = schema.ID mapperSchema := schema if schema.InternalSchema != nil { mapperSchema = schema.InternalSchema } for name, field := range mapperSchema.ResourceFields { fieldType := field.Type targetMap := t.subSchemas if definition.IsArrayType(fieldType) { fieldType = definition.SubType(fieldType) targetMap = t.subArraySchemas } else if definition.IsMapType(fieldType) { fieldType = definition.SubType(fieldType) targetMap = t.subMapSchemas } schema := schemas.doSchema(fieldType, false) if schema != nil { targetMap[name] = schema } } return Mappers(t.Mappers).ModifySchema(schema, schemas) } ================================================ FILE: pkg/schemas/mappers/access.go ================================================ package mappers import ( "strings" "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type Access struct { Fields map[string]string Optional bool } func (e Access) FromInternal(data data.Object) { } func (e Access) ToInternal(data data.Object) error { return nil } func (e Access) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { for name, access := range e.Fields { if err := ValidateField(name, schema); err != nil { if e.Optional { continue } return err } field := schema.ResourceFields[name] field.Create = strings.Contains(access, "c") field.Update = strings.Contains(access, "u") field.WriteOnly = strings.Contains(access, "o") schema.ResourceFields[name] = field } return nil } ================================================ FILE: pkg/schemas/mappers/alias.go ================================================ package mappers import ( "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type AliasField struct { Field string Names []string } func NewAlias(field string, names ...string) types.Mapper { return AliasField{ Field: field, Names: names, } } func (d AliasField) FromInternal(data data.Object) { } func (d AliasField) ToInternal(data data.Object) error { for _, name := range d.Names { if v, ok := data[name]; ok { delete(data, name) data[d.Field] = v } } return nil } func (d AliasField) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { for _, name := range d.Names { schema.ResourceFields[name] = types.Field{} } return ValidateField(d.Field, schema) } ================================================ FILE: pkg/schemas/mappers/check.go ================================================ package mappers import ( "fmt" types "github.com/rancher/wrangler/v3/pkg/schemas" ) func ValidateField(field string, schema *types.Schema) error { if _, ok := schema.ResourceFields[field]; !ok { return fmt.Errorf("field %s missing on schema %s", field, schema.ID) } return nil } ================================================ FILE: pkg/schemas/mappers/condition.go ================================================ package mappers import ( types "github.com/rancher/wrangler/v3/pkg/schemas" ) type Condition struct { Field string Value interface{} Mapper types.Mapper } func (m Condition) FromInternal(data map[string]interface{}) { if data[m.Field] == m.Value { m.Mapper.FromInternal(data) } } func (m Condition) ToInternal(data map[string]interface{}) error { if data[m.Field] == m.Value { return m.Mapper.ToInternal(data) } return nil } func (m Condition) ModifySchema(s *types.Schema, schemas *types.Schemas) error { return m.Mapper.ModifySchema(s, schemas) } ================================================ FILE: pkg/schemas/mappers/copy.go ================================================ package mappers import ( "fmt" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type Copy struct { From, To string } func (c Copy) FromInternal(data map[string]interface{}) { if data == nil { return } v, ok := data[c.From] if ok { data[c.To] = v } } func (c Copy) ToInternal(data map[string]interface{}) error { if data == nil { return nil } t, tok := data[c.To] _, fok := data[c.From] if tok && !fok { data[c.From] = t } return nil } func (c Copy) ModifySchema(s *types.Schema, schemas *types.Schemas) error { f, ok := s.ResourceFields[c.From] if !ok { return fmt.Errorf("field %s missing on schema %s", c.From, s.ID) } s.ResourceFields[c.To] = f return nil } ================================================ FILE: pkg/schemas/mappers/default.go ================================================ package mappers import ( "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type DefaultMapper struct { Field string } func (d DefaultMapper) FromInternal(data data.Object) { } func (d DefaultMapper) ToInternal(data data.Object) error { return nil } func (d DefaultMapper) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { if d.Field != "" { return ValidateField(d.Field, schema) } return nil } ================================================ FILE: pkg/schemas/mappers/drop.go ================================================ package mappers import ( "fmt" "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type Drop struct { Field string Optional bool } func (d Drop) FromInternal(data data.Object) { delete(data, d.Field) } func (d Drop) ToInternal(data data.Object) error { return nil } func (d Drop) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { if _, ok := schema.ResourceFields[d.Field]; !ok { if !d.Optional { return fmt.Errorf("can not drop missing field %s on %s", d.Field, schema.ID) } } delete(schema.ResourceFields, d.Field) return nil } ================================================ FILE: pkg/schemas/mappers/embed.go ================================================ package mappers import ( "fmt" "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type Embed struct { Field string Optional bool ReadOnly bool Ignore []string ignoreOverride bool embeddedFields []string EmptyValueOk bool } func (e *Embed) FromInternal(data data.Object) { sub := data.Map(e.Field) for _, fieldName := range e.embeddedFields { if v, ok := sub[fieldName]; ok { data[fieldName] = v } } delete(data, e.Field) } func (e *Embed) ToInternal(data data.Object) error { if data == nil { return nil } sub := map[string]interface{}{} for _, fieldName := range e.embeddedFields { if v, ok := data[fieldName]; ok { sub[fieldName] = v } delete(data, fieldName) } if len(sub) == 0 { if e.EmptyValueOk { data[e.Field] = nil } return nil } data[e.Field] = sub return nil } func (e *Embed) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { err := ValidateField(e.Field, schema) if err != nil { if e.Optional { return nil } return err } e.embeddedFields = []string{} embeddedSchemaID := schema.ResourceFields[e.Field].Type embeddedSchema := schemas.Schema(embeddedSchemaID) if embeddedSchema == nil { if e.Optional { return nil } return fmt.Errorf("failed to find schema %s for embedding", embeddedSchemaID) } deleteField := true outer: for name, field := range embeddedSchema.ResourceFields { for _, ignore := range e.Ignore { if ignore == name { continue outer } } if name == e.Field { deleteField = false } else { if !e.ignoreOverride { if _, ok := schema.ResourceFields[name]; ok { return fmt.Errorf("embedding field %s on %s will overwrite the field %s", e.Field, schema.ID, name) } } } if e.ReadOnly { field.Create = false field.Update = false } schema.ResourceFields[name] = field e.embeddedFields = append(e.embeddedFields, name) } if deleteField { delete(schema.ResourceFields, e.Field) } return nil } ================================================ FILE: pkg/schemas/mappers/empty.go ================================================ package mappers import ( "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/schemas" ) type EmptyMapper struct { } func (e *EmptyMapper) FromInternal(data data.Object) { } func (e *EmptyMapper) ToInternal(data data.Object) error { return nil } func (e *EmptyMapper) ModifySchema(schema *schemas.Schema, schemas *schemas.Schemas) error { return nil } ================================================ FILE: pkg/schemas/mappers/enum.go ================================================ package mappers import ( "fmt" "strings" "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/kv" "github.com/rancher/wrangler/v3/pkg/schemas" ) type Enum struct { DefaultMapper vals map[string]string } func NewEnum(field string, vals ...string) schemas.Mapper { f := &Enum{ DefaultMapper: DefaultMapper{ Field: field, }, vals: map[string]string{}, } for _, v := range vals { k := v if strings.Contains(v, "=") { v, k = kv.Split(v, "=") } f.vals[normalize(v)] = k } return f } func normalize(v string) string { v = strings.ReplaceAll(v, "_", "") v = strings.ReplaceAll(v, "-", "") return strings.ToLower(v) } func (d *Enum) FromInternal(data data.Object) { } func (d *Enum) ToInternal(data data.Object) error { v, ok := data[d.Field] if ok { newValue, ok := d.vals[normalize(convert.ToString(v))] if !ok { return fmt.Errorf("%s is not a valid value for field %s", v, d.Field) } data[d.Field] = newValue } return nil } ================================================ FILE: pkg/schemas/mappers/exists.go ================================================ package mappers import ( "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type Exists struct { Field string Mapper types.Mapper enabled bool } func (m *Exists) FromInternal(data data.Object) { if m.enabled { m.Mapper.FromInternal(data) } } func (m *Exists) ToInternal(data data.Object) error { if m.enabled { return m.Mapper.ToInternal(data) } return nil } func (m *Exists) ModifySchema(s *types.Schema, schemas *types.Schemas) error { if _, ok := s.ResourceFields[m.Field]; ok { m.enabled = true return m.Mapper.ModifySchema(s, schemas) } return nil } ================================================ FILE: pkg/schemas/mappers/json_keys.go ================================================ package mappers import ( "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type JSONKeys struct { } func (d JSONKeys) FromInternal(data data.Object) { } func (d JSONKeys) ToInternal(data data.Object) error { for key, value := range data { newKey := convert.ToJSONKey(key) if newKey != key { data[newKey] = value delete(data, key) } } return nil } func (d JSONKeys) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { return nil } ================================================ FILE: pkg/schemas/mappers/metadata.go ================================================ package mappers import ( types "github.com/rancher/wrangler/v3/pkg/schemas" ) func NewMetadataMapper() types.Mapper { return types.Mappers{ Move{From: "name", To: "id", CodeName: "ID"}, Drop{Field: "namespace"}, Drop{Field: "generateName"}, Move{From: "uid", To: "uuid", CodeName: "UUID"}, Drop{Field: "resourceVersion"}, Drop{Field: "generation"}, Move{From: "creationTimestamp", To: "created"}, Move{From: "deletionTimestamp", To: "removed"}, Drop{Field: "deletionGracePeriodSeconds"}, Drop{Field: "initializers"}, Drop{Field: "finalizers"}, Drop{Field: "managedFields"}, Drop{Field: "ownerReferences"}, Drop{Field: "clusterName"}, Drop{Field: "selfLink"}, Access{ Fields: map[string]string{ "labels": "cu", "annotations": "cu", }, }, } } ================================================ FILE: pkg/schemas/mappers/move.go ================================================ package mappers import ( "fmt" "strings" "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" types "github.com/rancher/wrangler/v3/pkg/schemas" "github.com/rancher/wrangler/v3/pkg/schemas/definition" ) type Move struct { Optional bool From, To, CodeName string DestDefined bool NoDeleteFromField bool } func (m Move) FromInternal(d data.Object) { if v, ok := data.RemoveValue(d, strings.Split(m.From, "/")...); ok { data.PutValue(d, v, strings.Split(m.To, "/")...) } } func (m Move) ToInternal(d data.Object) error { if v, ok := data.RemoveValue(d, strings.Split(m.To, "/")...); ok { data.PutValue(d, v, strings.Split(m.From, "/")...) } return nil } func (m Move) ModifySchema(s *types.Schema, schemas *types.Schemas) error { fromSchema, _, fromField, ok, err := getField(s, schemas, m.From) if err != nil { return err } if !ok { if m.Optional { return nil } return fmt.Errorf("failed to find field %s on schema %s", m.From, s.ID) } toSchema, toFieldName, _, _, err := getField(s, schemas, m.To) if err != nil { return err } _, ok = toSchema.ResourceFields[toFieldName] if ok && !strings.Contains(m.To, "/") && !m.DestDefined { return fmt.Errorf("field %s already exists on schema %s", m.To, s.ID) } if !m.NoDeleteFromField { delete(fromSchema.ResourceFields, m.From) } if !m.DestDefined { if m.CodeName == "" { fromField.CodeName = convert.Capitalize(toFieldName) } else { fromField.CodeName = m.CodeName } toSchema.ResourceFields[toFieldName] = fromField } return nil } func getField(schema *types.Schema, schemas *types.Schemas, target string) (*types.Schema, string, types.Field, bool, error) { parts := strings.Split(target, "/") for i, part := range parts { if i == len(parts)-1 { continue } fieldType := schema.ResourceFields[part].Type if definition.IsArrayType(fieldType) { fieldType = definition.SubType(fieldType) } subSchema := schemas.Schema(fieldType) if subSchema == nil { return nil, "", types.Field{}, false, fmt.Errorf("failed to find field or schema for %s on %s", part, schema.ID) } schema = subSchema } name := parts[len(parts)-1] f, ok := schema.ResourceFields[name] return schema, name, f, ok, nil } ================================================ FILE: pkg/schemas/mappers/set_value.go ================================================ package mappers import ( "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" ) type SetValue struct { Field string InternalValue interface{} ExternalValue interface{} } func (d SetValue) FromInternal(data data.Object) { if data != nil && d.ExternalValue != nil { data[d.Field] = d.ExternalValue } } func (d SetValue) ToInternal(data data.Object) error { if data != nil && d.InternalValue != nil { data[d.Field] = d.InternalValue } return nil } func (d SetValue) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { return ValidateField(d.Field, schema) } ================================================ FILE: pkg/schemas/mappers/slice_to_map.go ================================================ package mappers import ( "fmt" "github.com/rancher/wrangler/v3/pkg/data" types "github.com/rancher/wrangler/v3/pkg/schemas" "github.com/rancher/wrangler/v3/pkg/schemas/definition" ) type SliceToMap struct { Field string Key string } func (s SliceToMap) FromInternal(data data.Object) { datas := data.Slice(s.Field) result := map[string]interface{}{} for _, item := range datas { name, _ := item[s.Key].(string) delete(item, s.Key) result[name] = item } if len(result) > 0 { data[s.Field] = result } } func (s SliceToMap) ToInternal(data data.Object) error { datas := data.Map(s.Field) var result []interface{} for name, item := range datas { mapItem, _ := item.(map[string]interface{}) if mapItem != nil { mapItem[s.Key] = name result = append(result, mapItem) } } if len(result) > 0 { data[s.Field] = result } else if datas != nil { data[s.Field] = result } return nil } func (s SliceToMap) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { err := ValidateField(s.Field, schema) if err != nil { return err } subSchema, subFieldName, _, _, err := getField(schema, schemas, fmt.Sprintf("%s/%s", s.Field, s.Key)) if err != nil { return err } field := schema.ResourceFields[s.Field] if !definition.IsArrayType(field.Type) { return fmt.Errorf("field %s on %s is not an array", s.Field, schema.ID) } field.Type = "map[" + definition.SubType(field.Type) + "]" schema.ResourceFields[s.Field] = field delete(subSchema.ResourceFields, subFieldName) return nil } ================================================ FILE: pkg/schemas/openapi/generate.go ================================================ package openapi import ( "encoding/json" "fmt" "sort" "strings" types "github.com/rancher/wrangler/v3/pkg/schemas" "github.com/rancher/wrangler/v3/pkg/schemas/definition" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) func MustGenerate(obj interface{}) *v1.JSONSchemaProps { if obj == nil { return nil } result, err := ToOpenAPIFromStruct(obj) if err != nil { panic(err) } return result } func ToOpenAPIFromStruct(obj interface{}) (*v1.JSONSchemaProps, error) { schemas := types.EmptySchemas() schema, err := schemas.Import(obj) if err != nil { return nil, err } return ToOpenAPI(schema.ID, schemas) } func ToOpenAPI(name string, schemas *types.Schemas) (*v1.JSONSchemaProps, error) { schema := schemas.Schema(name) if schema == nil { return nil, fmt.Errorf("failed to find schema: %s", name) } newSchema := schema.DeepCopy() if newSchema.InternalSchema != nil { newSchema = newSchema.InternalSchema.DeepCopy() } delete(newSchema.ResourceFields, "kind") delete(newSchema.ResourceFields, "apiVersion") delete(newSchema.ResourceFields, "metadata") return schemaToProps(newSchema, schemas, map[string]bool{}) } func populateField(fieldJSP *v1.JSONSchemaProps, f *types.Field) error { fieldJSP.Description = f.Description // don't reset this to not nullable if f.Nullable { fieldJSP.Nullable = f.Nullable } fieldJSP.MinLength = f.MinLength fieldJSP.MaxLength = f.MaxLength if f.Type == "string" && len(f.Options) > 0 { for _, opt := range append(f.Options, "") { bytes, err := json.Marshal(&opt) if err != nil { return err } fieldJSP.Enum = append(fieldJSP.Enum, v1.JSON{ Raw: bytes, }) } } if len(f.InvalidChars) > 0 { fieldJSP.Pattern = fmt.Sprintf("^[^%s]*$", f.InvalidChars) } if len(f.ValidChars) > 0 { fieldJSP.Pattern = fmt.Sprintf("^[%s]*$", f.ValidChars) } if f.Min != nil { fl := float64(*f.Min) fieldJSP.Minimum = &fl } if f.Max != nil { fl := float64(*f.Max) fieldJSP.Maximum = &fl } if f.Default != nil { bytes, err := json.Marshal(f.Default) if err != nil { return err } fieldJSP.Default = &v1.JSON{ Raw: bytes, } } return nil } func typeToProps(typeName string, schemas *types.Schemas, inflight map[string]bool) (*v1.JSONSchemaProps, error) { t, subType, schema, err := typeAndSchema(typeName, schemas) if err != nil { return nil, err } if schema != nil { return schemaToProps(schema, schemas, inflight) } jsp := &v1.JSONSchemaProps{} switch t { case "map": additionalProps, err := typeToProps(subType, schemas, inflight) if err != nil { return nil, err } jsp.Type = "object" jsp.Nullable = true if subType != "json" { jsp.AdditionalProperties = &v1.JSONSchemaPropsOrBool{ Allows: true, Schema: additionalProps, } } case "array": items, err := typeToProps(subType, schemas, inflight) if err != nil { return nil, err } jsp.Type = "array" jsp.Nullable = true jsp.Items = &v1.JSONSchemaPropsOrArray{ Schema: items, } case "string": jsp.Type = t jsp.Nullable = true case "intOrString": jsp.XIntOrString = true default: jsp.Type = t } if jsp.Type == "object" && jsp.AdditionalProperties == nil { jsp.XPreserveUnknownFields = &[]bool{true}[0] } return jsp, nil } func schemaToProps(schema *types.Schema, schemas *types.Schemas, inflight map[string]bool) (*v1.JSONSchemaProps, error) { jsp := &v1.JSONSchemaProps{ Description: schema.Description, Type: "object", } if inflight[schema.ID] { return jsp, nil } inflight[schema.ID] = true defer delete(inflight, schema.ID) jsp.Properties = map[string]v1.JSONSchemaProps{} for name, f := range schema.ResourceFields { fieldJSP, err := typeToProps(f.Type, schemas, inflight) if err != nil { return nil, err } if err := populateField(fieldJSP, &f); err != nil { return nil, err } if f.Required { jsp.Required = append(jsp.Required, name) } jsp.Properties[name] = *fieldJSP } sort.Strings(jsp.Required) if len(jsp.Properties) == 0 && strings.HasSuffix(strings.ToLower(schema.ID), "map") { jsp.XPreserveUnknownFields = &[]bool{true}[0] } return jsp, nil } func typeAndSchema(typeName string, schemas *types.Schemas) (string, string, *types.Schema, error) { if definition.IsReferenceType(typeName) { return "string", "", nil, nil } if definition.IsArrayType(typeName) { return "array", definition.SubType(typeName), nil, nil } if definition.IsMapType(typeName) { return "map", definition.SubType(typeName), nil, nil } switch typeName { case "intOrString": return "intOrString", "", nil, nil case "int": return "integer", "", nil, nil case "float": return "number", "", nil, nil case "string": return "string", "", nil, nil case "date": return "string", "", nil, nil case "enum": return "string", "", nil, nil case "base64": return "string", "", nil, nil case "password": return "string", "", nil, nil case "hostname": return "string", "", nil, nil case "boolean": return "boolean", "", nil, nil case "json": return "object", "", nil, nil } schema := schemas.Schema(typeName) if schema == nil { return "", "", nil, fmt.Errorf("failed to find schema %s", typeName) } if schema.InternalSchema != nil { return "", "", schema.InternalSchema, nil } return "", "", schema, nil } ================================================ FILE: pkg/schemas/reflection.go ================================================ package schemas import ( "fmt" "reflect" "strconv" "strings" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/slice" "github.com/sirupsen/logrus" ) var ( skippedNames = map[string]bool{ "links": true, "actions": true, } ) func (s *Schemas) TypeName(name string, obj interface{}) *Schemas { s.typeNames[reflect.TypeOf(obj)] = name return s } func (s *Schemas) getTypeName(t reflect.Type) string { if name, ok := s.typeNames[t]; ok { return name } return convert.LowerTitle(t.Name()) } func (s *Schemas) SchemaFor(t reflect.Type) *Schema { name := s.getTypeName(t) return s.Schema(name) } func (s *Schemas) AddMapperForType(obj interface{}, mapper ...Mapper) *Schemas { if len(mapper) == 0 { return s } t := reflect.TypeOf(obj) typeName := s.getTypeName(t) if len(mapper) == 1 { return s.AddMapper(typeName, mapper[0]) } return s.AddMapper(typeName, Mappers(mapper)) } func (s *Schemas) MustImport(obj interface{}, externalOverrides ...interface{}) *Schemas { if reflect.ValueOf(obj).Kind() == reflect.Ptr { panic(fmt.Errorf("obj cannot be a pointer")) } if _, err := s.Import(obj, externalOverrides...); err != nil { panic(err) } return s } func (s *Schemas) MustImportAndCustomize(obj interface{}, f func(*Schema), externalOverrides ...interface{}) *Schemas { return s.MustImport(obj, externalOverrides...). MustCustomizeType(obj, f) } func getType(obj interface{}) reflect.Type { if t, ok := obj.(reflect.Type); ok { return t } t := reflect.TypeOf(obj) if t.Kind() == reflect.Ptr { t = t.Elem() } return t } func (s *Schemas) Import(obj interface{}, externalOverrides ...interface{}) (*Schema, error) { var types []reflect.Type for _, override := range externalOverrides { types = append(types, getType(override)) } t := getType(obj) return s.importType(t, types...) } func (s *Schemas) newSchemaFromType(t reflect.Type, typeName string) (*Schema, error) { schema := &Schema{ ID: typeName, CodeName: t.Name(), PkgName: t.PkgPath(), ResourceFields: map[string]Field{}, ResourceActions: map[string]Action{}, CollectionActions: map[string]Action{}, Attributes: map[string]interface{}{}, ResourcePermissions: ResourcePermissions{}, } s.processingTypes[t] = schema defer delete(s.processingTypes, t) if err := s.readFields(schema, t); err != nil { return nil, err } return schema, nil } func (s *Schemas) MustCustomizeType(obj interface{}, f func(*Schema)) *Schemas { name := s.getTypeName(reflect.TypeOf(obj)) schema := s.Schema(name) if schema == nil { panic("Failed to find schema " + name) } f(schema) return s } func (s *Schemas) assignMappers(schema *Schema) error { if schema.Mapper != nil { return nil } mappers := s.mapper(schema.ID) if canList(schema) { if s.DefaultMapper != nil { mappers = append([]Mapper{s.DefaultMapper()}, mappers...) } if s.DefaultPostMapper != nil { mappers = append(mappers, s.DefaultPostMapper()) } } if len(mappers) > 0 { schema.InternalSchema = schema.DeepCopy() } mapper := &typeMapper{ Mappers: mappers, root: canList(schema), } if err := mapper.ModifySchema(schema, s); err != nil { return err } schema.Mapper = mapper return nil } func canList(schema *Schema) bool { return slice.ContainsString(schema.CollectionMethods, "GET") } func (s *Schemas) importType(t reflect.Type, overrides ...reflect.Type) (*Schema, error) { typeName := s.getTypeName(t) existing := s.Schema(typeName) if existing != nil { return existing, nil } if s, ok := s.processingTypes[t]; ok { logrus.Debugf("Returning half built schema %s for %v", typeName, t) return s, nil } logrus.Tracef("Inspecting schema %s for %v", typeName, t) schema, err := s.newSchemaFromType(t, typeName) if err != nil { return nil, err } for _, override := range overrides { if err := s.readFields(schema, override); err != nil { return nil, err } } if err := s.assignMappers(schema); err != nil { return nil, err } err = s.AddSchema(*schema) return s.Schema(schema.ID), err } func jsonName(f reflect.StructField) string { return strings.SplitN(f.Tag.Get("json"), ",", 2)[0] } func k8sType(field reflect.StructField) bool { return field.Type.Name() == "TypeMeta" && strings.HasSuffix(field.Type.PkgPath(), "k8s.io/apimachinery/pkg/apis/meta/v1") } func k8sObject(field reflect.StructField) bool { return field.Type.Name() == "ObjectMeta" && strings.HasSuffix(field.Type.PkgPath(), "k8s.io/apimachinery/pkg/apis/meta/v1") } func (s *Schemas) readFields(schema *Schema, t reflect.Type) error { hasType := false hasMeta := false for i := 0; i < t.NumField(); i++ { field := t.Field(i) if field.PkgPath != "" { // unexported field continue } jsonName := jsonName(field) if jsonName == "-" { continue } if field.Anonymous && jsonName == "" && k8sType(field) { hasType = true } if field.Anonymous && jsonName == "metadata" && k8sObject(field) { hasMeta = true } if field.Anonymous && jsonName == "" { t := field.Type if t.Kind() == reflect.Ptr { t = t.Elem() } if t.Kind() == reflect.Struct { if err := s.readFields(schema, t); err != nil { return err } } continue } fieldName := jsonName if fieldName == "" { fieldName = convert.LowerTitle(field.Name) if strings.HasSuffix(fieldName, "ID") { fieldName = strings.TrimSuffix(fieldName, "ID") + "Id" } } if skippedNames[fieldName] { logrus.Debugf("Ignoring skip field %s.%s for %v", schema.ID, fieldName, field) continue } logrus.Tracef("Inspecting field %s.%s for %v", schema.ID, fieldName, field) schemaField := Field{ CodeName: field.Name, Create: true, Update: true, } fieldType := field.Type if fieldType.Kind() == reflect.Ptr { schemaField.Nullable = true fieldType = fieldType.Elem() } else if fieldType.Kind() == reflect.Bool { schemaField.Nullable = false } else if fieldType.Kind() == reflect.Int || fieldType.Kind() == reflect.Uint || fieldType.Kind() == reflect.Uintptr || fieldType.Kind() == reflect.Uint32 || fieldType.Kind() == reflect.Int32 || fieldType.Kind() == reflect.Uint64 || fieldType.Kind() == reflect.Int64 || fieldType.Kind() == reflect.Float32 || fieldType.Kind() == reflect.Float64 { schemaField.Nullable = false } if err := applyTag(&field, &schemaField); err != nil { return err } if schemaField.Type == "" { inferredType, err := s.determineSchemaType(fieldType) if err != nil { return fmt.Errorf("failed inspecting type %s, field %s: %v", t, fieldName, err) } schemaField.Type = inferredType } if schemaField.Default != nil { switch schemaField.Type { case "int": n, err := convert.ToNumber(schemaField.Default) if err != nil { return err } schemaField.Default = n case "float": n, err := convert.ToFloat(schemaField.Default) if err != nil { return err } schemaField.Default = n case "boolean": schemaField.Default = convert.ToBool(schemaField.Default) } } if s.fieldMappers != nil { if err := s.processFieldsMappers(t, fieldName, schema, field); err != nil { return err } } logrus.Tracef("Setting field %s.%s: %#v", schema.ID, fieldName, schemaField) schema.ResourceFields[fieldName] = schemaField } if hasType && hasMeta { delete(schema.ResourceFields, "kind") delete(schema.ResourceFields, "apiVersion") delete(schema.ResourceFields, "metadata") schema.CollectionMethods = []string{"GET", "POST"} schema.ResourceMethods = []string{"GET", "PUT", "DELETE"} } return nil } func (s *Schemas) processFieldsMappers(t reflect.Type, fieldName string, schema *Schema, field reflect.StructField) error { for _, fieldMapper := range strings.Split(field.Tag.Get("mapper"), ",") { if fieldMapper == "" { continue } var ( name string opts []string ) parts := strings.SplitN(fieldMapper, "=", 2) name = parts[0] if len(parts) == 2 { opts = append(opts, strings.Split(parts[1], "|")...) } factory, ok := s.fieldMappers[name] if !ok { return fmt.Errorf("failed to find field mapper [%s] for type [%v]", name, t) } s.AddMapper(schema.ID, factory(fieldName, opts...)) } return nil } func applyTag(structField *reflect.StructField, field *Field) error { t := structField.Tag.Get("wrangler") for _, part := range strings.Split(t, ",") { if part == "" { continue } var err error key, value := getKeyValue(part) switch key { case "type": field.Type = value case "codeName": field.CodeName = value case "default": field.Default = value case "nullable": field.Nullable = true case "notnullable": field.Nullable = false case "create": field.Create = true case "nocreate": field.Create = false case "writeOnly": field.WriteOnly = true case "required": field.Required = true case "update": field.Update = true case "noupdate": field.Update = false case "minLength": field.MinLength, err = toInt(value, structField) case "maxLength": field.MaxLength, err = toInt(value, structField) case "min": field.Min, err = toInt(value, structField) case "max": field.Max, err = toInt(value, structField) case "options": field.Options = split(value) if field.Type == "" { field.Type = "enum" } case "validChars": field.ValidChars = value case "invalidChars": field.InvalidChars = value default: return fmt.Errorf("invalid tag %s on field %s", key, structField.Name) } if err != nil { return err } } return nil } func toInt(value string, structField *reflect.StructField) (*int64, error) { i, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, fmt.Errorf("invalid number on field %s: %v", structField.Name, err) } return &i, nil } func split(input string) []string { var result []string for _, i := range strings.Split(input, "|") { for _, part := range strings.Split(i, " ") { part = strings.TrimSpace(part) if len(part) > 0 { result = append(result, part) } } } return result } func getKeyValue(input string) (string, string) { var ( key, value string ) parts := strings.SplitN(input, "=", 2) key = parts[0] if len(parts) > 1 { value = parts[1] } return key, value } func deRef(p reflect.Type) reflect.Type { if p.Kind() == reflect.Ptr { return p.Elem() } return p } func (s *Schemas) determineSchemaType(t reflect.Type) (string, error) { switch t.Kind() { case reflect.Uint8: return "byte", nil case reflect.Bool: return "boolean", nil case reflect.Int, reflect.Int32, reflect.Uint, reflect.Uintptr, reflect.Uint32, reflect.Uint64, reflect.Int64: return "int", nil case reflect.Float32, reflect.Float64: return "float", nil case reflect.Interface: return "json", nil case reflect.Map: subType, err := s.determineSchemaType(deRef(t.Elem())) if err != nil { return "", err } return fmt.Sprintf("map[%s]", subType), nil case reflect.Slice: subType, err := s.determineSchemaType(deRef(t.Elem())) if err != nil { return "", err } if subType == "byte" { return "base64", nil } return fmt.Sprintf("array[%s]", subType), nil case reflect.String: return "string", nil case reflect.Struct: if t.Name() == "Time" { return "date", nil } if t.Name() == "IntOrString" { return "intOrString", nil } if t.Name() == "Quantity" { return "string", nil } schema, err := s.importType(t) if err != nil { return "", err } if t.Name() == "Duration" && strings.Contains(schema.PkgName, "k8s.io/apimachinery/pkg/apis/meta/v1") { return "string", nil } return schema.ID, nil default: return "", fmt.Errorf("unknown type kind %s", t.Kind()) } } ================================================ FILE: pkg/schemas/schemas.go ================================================ package schemas import ( "fmt" "reflect" "strings" "sync" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/merr" "github.com/rancher/wrangler/v3/pkg/name" ) type SchemasInitFunc func(*Schemas) *Schemas type MapperFactory func() Mapper type FieldMapperFactory func(fieldName string, args ...string) Mapper type Schemas struct { sync.Mutex processingTypes map[reflect.Type]*Schema typeNames map[reflect.Type]string schemasByID map[string]*Schema mappers map[string][]Mapper embedded map[string]*Schema fieldMappers map[string]FieldMapperFactory DefaultMapper MapperFactory DefaultPostMapper MapperFactory schemas []*Schema } func EmptySchemas() *Schemas { s, _ := NewSchemas() return s } func NewSchemas(schemas ...*Schemas) (*Schemas, error) { var ( errs []error ) s := &Schemas{ processingTypes: map[reflect.Type]*Schema{}, typeNames: map[reflect.Type]string{}, schemasByID: map[string]*Schema{}, mappers: map[string][]Mapper{}, embedded: map[string]*Schema{}, } for _, schemas := range schemas { if _, err := s.AddSchemas(schemas); err != nil { errs = append(errs, err) } } return s, merr.NewErrors(errs...) } func (s *Schemas) Init(initFunc SchemasInitFunc) *Schemas { return initFunc(s) } func (s *Schemas) MustAddSchemas(schema *Schemas) *Schemas { s, err := s.AddSchemas(schema) if err != nil { panic(err) } return s } func (s *Schemas) AddSchemas(schema *Schemas) (*Schemas, error) { var errs []error for _, schema := range schema.Schemas() { if err := s.AddSchema(*schema); err != nil { errs = append(errs, err) } } return s, merr.NewErrors(errs...) } func (s *Schemas) RemoveSchema(schema Schema) *Schemas { s.Lock() defer s.Unlock() return s.doRemoveSchema(schema) } func (s *Schemas) doRemoveSchema(schema Schema) *Schemas { delete(s.schemasByID, schema.ID) return s } func (s *Schemas) MustAddSchema(schema Schema) *Schemas { err := s.AddSchema(schema) if err != nil { panic(err) } return s } func (s *Schemas) AddSchema(schema Schema) error { s.Lock() defer s.Unlock() return s.doAddSchema(schema) } func (s *Schemas) doAddSchema(schema Schema) error { if err := s.setupDefaults(&schema); err != nil { return err } existing, ok := s.schemasByID[schema.ID] if ok { *existing = schema } else { s.schemasByID[schema.ID] = &schema s.schemas = append(s.schemas, &schema) } return nil } func (s *Schemas) setupDefaults(schema *Schema) (err error) { if schema.ID == "" { return fmt.Errorf("ID is not set on schema: %v", schema) } if schema.PluralName == "" { schema.PluralName = name.GuessPluralName(schema.ID) } if schema.CodeName == "" { schema.CodeName = convert.Capitalize(schema.ID) } if schema.CodeNamePlural == "" { schema.CodeNamePlural = name.GuessPluralName(schema.CodeName) } if err := s.assignMappers(schema); err != nil { return err } return } func (s *Schemas) AddFieldMapper(name string, factory FieldMapperFactory) *Schemas { if s.fieldMappers == nil { s.fieldMappers = map[string]FieldMapperFactory{} } s.fieldMappers[name] = factory return s } func (s *Schemas) AddMapper(schemaID string, mapper Mapper) *Schemas { s.mappers[schemaID] = append(s.mappers[schemaID], mapper) return s } func (s *Schemas) Schemas() []*Schema { return s.schemas } func (s *Schemas) SchemasByID() map[string]*Schema { return s.schemasByID } func (s *Schemas) mapper(schemaID string) []Mapper { return s.mappers[schemaID] } func (s *Schemas) Schema(name string) *Schema { return s.doSchema(name, true) } func (s *Schemas) doSchema(name string, lock bool) *Schema { if lock { s.Lock() } schema, ok := s.schemasByID[name] if lock { s.Unlock() } if ok { return schema } for _, check := range s.schemas { if strings.EqualFold(check.ID, name) || strings.EqualFold(check.PluralName, name) { return check } } return nil } func (s *Schema) MustCustomizeField(name string, f func(f Field) Field) *Schema { field, ok := s.ResourceFields[name] if !ok { panic("Failed to find field " + name + " on schema " + s.ID) } s.ResourceFields[name] = f(field) return s } ================================================ FILE: pkg/schemas/types.go ================================================ package schemas import ( "github.com/rancher/wrangler/v3/pkg/data/convert" ) type Schema struct { ID string `json:"-"` Description string `json:"description,omitempty"` CodeName string `json:"-"` CodeNamePlural string `json:"-"` PkgName string `json:"-"` PluralName string `json:"pluralName,omitempty"` ResourceMethods []string `json:"resourceMethods,omitempty"` ResourceFields map[string]Field `json:"resourceFields"` ResourceActions map[string]Action `json:"resourceActions,omitempty"` CollectionMethods []string `json:"collectionMethods,omitempty"` CollectionFields map[string]Field `json:"collectionFields,omitempty"` CollectionActions map[string]Action `json:"collectionActions,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` ResourcePermissions ResourcePermissions `json:"resourcePermissions,omitempty"` InternalSchema *Schema `json:"-"` Mapper Mapper `json:"-"` } func (s *Schema) DeepCopy() *Schema { r := *s if s.ResourceFields != nil { r.ResourceFields = map[string]Field{} for k, v := range s.ResourceFields { r.ResourceFields[k] = v } } if s.ResourceActions != nil { r.ResourceActions = map[string]Action{} for k, v := range s.ResourceActions { r.ResourceActions[k] = v } } if s.CollectionFields != nil { r.CollectionFields = map[string]Field{} for k, v := range s.CollectionFields { r.CollectionFields[k] = v } } if s.CollectionActions != nil { r.CollectionActions = map[string]Action{} for k, v := range s.CollectionActions { r.CollectionActions[k] = v } } if s.Attributes != nil { r.Attributes = map[string]interface{}{} for k, v := range s.Attributes { r.Attributes[k] = v } } if s.ResourcePermissions != nil { r.ResourcePermissions = make(ResourcePermissions) for res, perms := range s.ResourcePermissions { permCopy := make(ResourceVerbs) for verb, url := range perms { permCopy[verb] = url } r.ResourcePermissions[res] = permCopy } } if s.InternalSchema != nil { r.InternalSchema = r.InternalSchema.DeepCopy() } return &r } func SetHasObservedGeneration(s *Schema, value bool) { if s == nil { return } if s.Attributes == nil { s.Attributes = map[string]interface{}{} } s.Attributes["hasObservedGeneration"] = value } func HasObservedGeneration(s *Schema) bool { if s == nil || s.Attributes == nil { return false } return convert.ToBool(s.Attributes["hasObservedGeneration"]) } type Field struct { Type string `json:"type,omitempty"` Default interface{} `json:"default,omitempty"` Nullable bool `json:"nullable,omitempty"` Create bool `json:"create"` WriteOnly bool `json:"writeOnly,omitempty"` Required bool `json:"required,omitempty"` Update bool `json:"update"` MinLength *int64 `json:"minLength,omitempty"` MaxLength *int64 `json:"maxLength,omitempty"` Min *int64 `json:"min,omitempty"` Max *int64 `json:"max,omitempty"` Options []string `json:"options,omitempty"` ValidChars string `json:"validChars,omitempty"` InvalidChars string `json:"invalidChars,omitempty"` Description string `json:"description,omitempty"` CodeName string `json:"-"` } type Action struct { Input string `json:"input,omitempty"` Output string `json:"output,omitempty"` } type ResourcePermissions map[string]ResourceVerbs type ResourceVerbs map[string]string ================================================ FILE: pkg/schemas/validation/error.go ================================================ package validation import ( "errors" "fmt" ) var ( Unauthorized = ErrorCode{"Unauthorized", 401} PermissionDenied = ErrorCode{"PermissionDenied", 403} NotFound = ErrorCode{"NotFound", 404} MethodNotAllowed = ErrorCode{"MethodNotAllowed", 405} Conflict = ErrorCode{"Conflict", 409} InvalidDateFormat = ErrorCode{"InvalidDateFormat", 422} InvalidFormat = ErrorCode{"InvalidFormat", 422} InvalidReference = ErrorCode{"InvalidReference", 422} NotNullable = ErrorCode{"NotNullable", 422} NotUnique = ErrorCode{"NotUnique", 422} MinLimitExceeded = ErrorCode{"MinLimitExceeded", 422} MaxLimitExceeded = ErrorCode{"MaxLimitExceeded", 422} MinLengthExceeded = ErrorCode{"MinLengthExceeded", 422} MaxLengthExceeded = ErrorCode{"MaxLengthExceeded", 422} InvalidOption = ErrorCode{"InvalidOption", 422} InvalidCharacters = ErrorCode{"InvalidCharacters", 422} MissingRequired = ErrorCode{"MissingRequired", 422} InvalidCSRFToken = ErrorCode{"InvalidCSRFToken", 422} InvalidAction = ErrorCode{"InvalidAction", 422} InvalidBodyContent = ErrorCode{"InvalidBodyContent", 422} InvalidType = ErrorCode{"InvalidType", 422} ActionNotAvailable = ErrorCode{"ActionNotAvailable", 404} InvalidState = ErrorCode{"InvalidState", 422} ServerError = ErrorCode{"ServerError", 500} ClusterUnavailable = ErrorCode{"ClusterUnavailable", 503} ErrComplete = errors.New("request completed") ) type ErrorCode struct { Code string Status int } func (e ErrorCode) Error() string { return fmt.Sprintf("%s %d", e.Code, e.Status) } ================================================ FILE: pkg/schemas/validation/validation.go ================================================ package validation import ( "errors" "fmt" "strings" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/schemas" "k8s.io/apimachinery/pkg/util/validation" ) var ( ErrComplexType = errors.New("complex type") ) func CheckFieldCriteria(fieldName string, field schemas.Field, value interface{}) error { numVal, isNum := value.(int64) strVal := "" hasStrVal := false if value == nil && field.Default != nil { value = field.Default } if value != nil && value != "" { hasStrVal = true strVal = fmt.Sprint(value) } if (value == nil || value == "") && !field.Nullable { if field.Default == nil { return NotNullable } } if isNum { if field.Min != nil && numVal < *field.Min { return MinLimitExceeded } if field.Max != nil && numVal > *field.Max { return MaxLimitExceeded } } if hasStrVal || value == "" { if field.MinLength != nil && int64(len(strVal)) < *field.MinLength { return MinLengthExceeded } if field.MaxLength != nil && int64(len(strVal)) > *field.MaxLength { return MaxLengthExceeded } } if len(field.Options) > 0 { if hasStrVal || !field.Nullable { found := false for _, option := range field.Options { if strVal == option { found = true break } } if !found { return InvalidOption } } } if len(field.ValidChars) > 0 && hasStrVal { for _, c := range strVal { if !strings.ContainsRune(field.ValidChars, c) { return InvalidCharacters } } } if len(field.InvalidChars) > 0 && hasStrVal { if strings.ContainsAny(strVal, field.InvalidChars) { return InvalidCharacters } } return nil } func ConvertSimple(fieldType string, value interface{}) (interface{}, error) { if value == nil { return value, nil } switch fieldType { case "json": return value, nil case "date": v := convert.ToString(value) if v == "" { return nil, nil } return v, nil case "boolean": return convert.ToBool(value), nil case "enum": return convert.ToString(value), nil case "int": return convert.ToNumber(value) case "float": return convert.ToFloat(value) case "password": return convert.ToString(value), nil case "string": return convert.ToString(value), nil case "dnsLabel": str := convert.ToString(value) if str == "" { return "", nil } if errs := validation.IsDNS1123Label(str); len(errs) != 0 { return nil, InvalidFormat } return str, nil case "dnsLabelRestricted": str := convert.ToString(value) if str == "" { return "", nil } if errs := validation.IsDNS1035Label(str); len(errs) != 0 { return value, InvalidFormat } return str, nil case "hostname": str := convert.ToString(value) if str == "" { return "", nil } if errs := validation.IsDNS1123Subdomain(str); len(errs) != 0 { return value, InvalidFormat } return str, nil case "intOrString": num, err := convert.ToNumber(value) if err == nil { return num, nil } return convert.ToString(value), nil case "base64": return convert.ToString(value), nil case "reference": return convert.ToString(value), nil } return nil, ErrComplexType } ================================================ FILE: pkg/schemes/all.go ================================================ package schemes import ( "github.com/rancher/lasso/pkg/scheme" "k8s.io/apimachinery/pkg/runtime" ) var ( All = scheme.All localSchemeBuilder = runtime.NewSchemeBuilder() ) func Register(addToScheme func(*runtime.Scheme) error) error { localSchemeBuilder = append(localSchemeBuilder, addToScheme) return addToScheme(All) } func AddToScheme(scheme *runtime.Scheme) error { return localSchemeBuilder.AddToScheme(scheme) } ================================================ FILE: pkg/seen/strings.go ================================================ package seen import "k8s.io/apimachinery/pkg/util/sets" type Seen interface { String(value string) bool } func New() Seen { return &strings{ s: sets.NewString(), } } type strings struct { s sets.String } func (s strings) String(value string) bool { if s.s.Has(value) { return true } s.s.Insert(value) return false } ================================================ FILE: pkg/signals/signal.go ================================================ /* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package signals import ( "context" "os" "os/signal" "github.com/sirupsen/logrus" ) var onlyOneSignalHandler = make(chan struct{}) var shutdownHandler chan os.Signal // SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned // which is closed on one of these signals. If a second signal is caught, the program // is terminated with exit code 1. // Only one of SetupSignalContext and SetupSignalHandler should be called, and only can // be called once. func SetupSignalHandler() <-chan struct{} { return SetupSignalContext().Done() } // SetupSignalContext is same as SetupSignalHandler, but a context.Context is returned. // Only one of SetupSignalContext and SetupSignalHandler should be called, and only can // be called once. func SetupSignalContext() context.Context { close(onlyOneSignalHandler) // panics when called twice shutdownHandler = make(chan os.Signal, 2) ctx, cancel := context.WithCancel(context.Background()) signal.Notify(shutdownHandler, shutdownSignals...) go func() { s := <-shutdownHandler logrus.Warnf("signal received: %q, canceling context...", s) cancel() s = <-shutdownHandler logrus.Warnf("second signal received: %q, exiting...", s) os.Exit(1) // second signal. Exit directly. }() return ctx } // RequestShutdown emulates a received event that is considered as shutdown signal (SIGTERM/SIGINT) // This returns whether a handler was notified func RequestShutdown() bool { if shutdownHandler != nil { select { case shutdownHandler <- shutdownSignals[0]: return true default: } } return false } ================================================ FILE: pkg/signals/signal_posix.go ================================================ //go:build !windows // +build !windows /* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package signals import ( "os" "syscall" ) var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} ================================================ FILE: pkg/signals/signal_windows.go ================================================ /* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package signals import ( "os" ) var shutdownSignals = []os.Signal{os.Interrupt} ================================================ FILE: pkg/slice/contains.go ================================================ package slice func ContainsString(slice []string, item string) bool { for _, j := range slice { if j == item { return true } } return false } func StringsEqual(left, right []string) bool { if len(left) != len(right) { return false } for i := 0; i < len(left); i++ { if left[i] != right[i] { return false } } return true } ================================================ FILE: pkg/start/all.go ================================================ package start import ( "context" "golang.org/x/sync/errgroup" ) type Starter interface { Sync(ctx context.Context) error Start(ctx context.Context, threadiness int) error } func All(ctx context.Context, threadiness int, starters ...Starter) error { if err := Sync(ctx, starters...); err != nil { return err } return Start(ctx, threadiness, starters...) } func Sync(ctx context.Context, starters ...Starter) error { eg, _ := errgroup.WithContext(ctx) for _, starter := range starters { func(starter Starter) { eg.Go(func() error { return starter.Sync(ctx) }) }(starter) } return eg.Wait() } func Start(ctx context.Context, threadiness int, starters ...Starter) error { for _, starter := range starters { if err := starter.Start(ctx, threadiness); err != nil { return err } } return nil } ================================================ FILE: pkg/stringset/stringset.go ================================================ package stringset var empty struct{} // Set is an exceptionally simple `set` implementation for strings. // It is not threadsafe, but can be used in place of a simple `map[string]struct{}` // as long as you don't want to do too much with it. type Set struct { m map[string]struct{} } func (s *Set) Add(ss ...string) { if s.m == nil { s.m = make(map[string]struct{}, len(ss)) } for _, k := range ss { s.m[k] = empty } } func (s *Set) Delete(ss ...string) { if s.m == nil { return } for _, k := range ss { delete(s.m, k) } } func (s *Set) Has(ss string) bool { if s.m == nil { return false } _, ok := s.m[ss] return ok } func (s *Set) Len() int { return len(s.m) } func (s *Set) Values() []string { i := 0 keys := make([]string, len(s.m)) for key := range s.m { keys[i] = key i++ } return keys } ================================================ FILE: pkg/stringset/stringset_test.go ================================================ package stringset import ( "testing" "github.com/stretchr/testify/assert" ) func stringPtr(s string) *string { return &s } func Test_Set(t *testing.T) { tests := []struct { name string addStrings [][]string deleteStrings [][]string hasString *string missingString *string finalStrings []string wantLen int }{ { name: "test 1", addStrings: [][]string{}, deleteStrings: [][]string{}, hasString: nil, missingString: stringPtr("bar"), finalStrings: []string{}, wantLen: 0, }, { name: "test 2", addStrings: [][]string{{"foo"}}, deleteStrings: [][]string{}, hasString: stringPtr("foo"), missingString: stringPtr("bar"), finalStrings: []string{"foo"}, wantLen: 1, }, { name: "test 3", addStrings: [][]string{{"foo", "bar", "baz"}, {"bar", "baz"}, {"bop"}}, deleteStrings: [][]string{{"foo", "baz"}}, hasString: stringPtr("bar"), missingString: stringPtr("foo"), finalStrings: []string{"bar", "bop"}, wantLen: 2, }, { name: "test 4", addStrings: [][]string{{"foo"}, {""}, {"bar"}}, deleteStrings: [][]string{{"bar"}}, hasString: stringPtr(""), missingString: stringPtr("bar"), finalStrings: []string{"foo", ""}, wantLen: 2, }, { name: "test 5", addStrings: [][]string{{"foo"}, {"foo", "bar"}, {"foo", "bar", "baz"}}, deleteStrings: [][]string{{"foo"}, {"bar", "baz"}}, hasString: nil, missingString: stringPtr("foo"), finalStrings: []string{}, wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { set := Set{} for _, ss := range tt.addStrings { set.Add(ss...) } for _, s := range tt.deleteStrings { set.Delete(s...) } if tt.hasString != nil { hasString := set.Has(*tt.hasString) assert.True(t, hasString, "HasString(%#v)", tt.hasString) } if tt.missingString != nil { missingString := set.Has(*tt.missingString) assert.False(t, missingString, "HasString(%#v)", tt.missingString) } gotStrings := set.Values() assert.ElementsMatchf(t, tt.finalStrings, gotStrings, "Values() = %v, want %v", gotStrings, tt.finalStrings) gotLen := set.Len() assert.Equal(t, tt.wantLen, gotLen, "Len() = %v, want %v", gotLen, tt.wantLen) }) } } ================================================ FILE: pkg/summary/capi_cluster_test.go ================================================ package summary import ( "testing" "github.com/rancher/wrangler/v3/pkg/data" "github.com/stretchr/testify/assert" ) func TestIsCAPICluster(t *testing.T) { tests := []struct { name string obj data.Object expected bool }{ { name: "CAPI v1beta2 Cluster", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, expected: true, }, { name: "CAPI v1beta1 Cluster", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta1", "kind": "Cluster", }, expected: true, }, { name: "CAPI v1beta2 Machine (not Cluster)", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Machine", }, expected: false, }, { name: "CAPI v1beta2 MachineSet (not Cluster)", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, expected: false, }, { name: "Rancher management Cluster (not CAPI)", obj: data.Object{ "apiVersion": "management.cattle.io/v3", "kind": "Cluster", }, expected: false, }, { name: "non-CAPI kind", obj: data.Object{ "apiVersion": "apps/v1", "kind": "Deployment", }, expected: false, }, { name: "empty object", obj: data.Object{}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, isCAPICluster(tt.obj)) }) } } // makeClusterObj builds a minimal CAPI Cluster data.Object with the given // worker replica fields under status.workers. Use -1 to omit a field. func makeClusterObj(desiredReplicas, replicas, readyReplicas, availableReplicas, upToDateReplicas int64) data.Object { obj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", "status": map[string]interface{}{ "workers": map[string]interface{}{}, }, } workers := obj["status"].(map[string]interface{})["workers"].(map[string]interface{}) if desiredReplicas >= 0 { workers["desiredReplicas"] = desiredReplicas } if replicas >= 0 { workers["replicas"] = replicas } if readyReplicas >= 0 { workers["readyReplicas"] = readyReplicas } if availableReplicas >= 0 { workers["availableReplicas"] = availableReplicas } if upToDateReplicas >= 0 { workers["upToDateReplicas"] = upToDateReplicas } return obj } func TestCheckCAPIClusterTransitioning(t *testing.T) { tests := []struct { name string obj data.Object conditions []Condition expectedState string expectedTransit bool expectedError bool expectedMessages []string }{ // --- Priority 1: Deleting --- { name: "Deleting=True takes absolute priority over everything", obj: makeClusterObj(1, 1, 1, 1, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "* Deleting: ..."), NewCondition("ScalingDown", "True", "ScalingDown", "* MachineDeployment md: Scaling down from 1 to 0 replicas"), NewCondition("Deleting", "True", "WaitingForWorkersDeletion", "* MachineDeployments: md\n* MachineSets: ms"), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"waiting for workers deletion"}, }, { name: "Deleting=True with empty message", obj: makeClusterObj(1, 1, 1, 1, -1), conditions: []Condition{ NewCondition("Deleting", "True", "Deleting", ""), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "Deleting=False does not trigger removing", obj: makeClusterObj(2, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "True", "Available", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Priority 2: Paused --- { name: "Paused=True takes priority over scaling and Available", obj: makeClusterObj(3, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "something"), NewCondition("ScalingUp", "True", "ScalingUp", "scaling"), NewCondition("Paused", "True", "Paused", "cluster is paused"), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, { name: "Paused=False is ignored", obj: makeClusterObj(2, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "True", "Available", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Priority 3: Rolling out --- { name: "RollingOut=True takes priority over ScalingDown during rolling upgrade", obj: makeClusterObj(4, 5, 5, 5, 3), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "* MachineDeployment do-check-jiaqi-dow:\n * Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "* MachineDeployment do-check-jiaqi-dow: Scaling down from 4 to 3 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Available", "True", "Available", ""), }, expectedState: "rollingout", expectedTransit: true, expectedError: false, expectedMessages: []string{"rolling out 2 not up-to-date replicas"}, }, { name: "RollingOut=True takes priority over ScalingUp during rolling upgrade", obj: makeClusterObj(4, 4, 3, 3, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "* MachineDeployment md:\n * Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Available", "False", "NotAvailable", "something"), }, expectedState: "rollingout", expectedTransit: true, expectedError: false, expectedMessages: []string{"rolling out 2 not up-to-date replicas"}, }, // --- Priority 4: ScalingDown --- { name: "ScalingDown=True — message constructed from worker replicas", obj: makeClusterObj(2, 3, 3, 3, -1), conditions: []Condition{ NewCondition("Available", "True", "Available", ""), NewCondition("ScalingDown", "True", "ScalingDown", "* MachineDeployment md: Scaling down from 2 to 1 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 3 to 2 machines"}, }, { name: "ScalingDown detected by replica mismatch only (stale condition)", obj: makeClusterObj(2, 3, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "True", "Available", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 3 to 2 machines"}, }, { name: "ScalingDown takes priority over ScalingUp when both detectable", obj: makeClusterObj(1, 3, 0, 0, -1), conditions: []Condition{ NewCondition("ScalingDown", "True", "ScalingDown", "scaling down"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 3 to 1 machines"}, }, // --- Priority 5: ScalingUp --- { name: "ScalingUp=True — scale-up scenario (2→3 workers)", obj: makeClusterObj(3, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "* WorkersAvailable: insufficient replicas"), NewCondition("ScalingUp", "True", "ScalingUp", "* MachineDeployment md: Scaling up from 1 to 2 replicas"), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 2 to 3 machines"}, }, { name: "ScalingUp detected by replica mismatch (stale condition)", obj: makeClusterObj(3, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "something"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 2 to 3 machines"}, }, { name: "ScalingUp during cluster creation (0→1 workers)", obj: makeClusterObj(1, -1, -1, -1, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "* WorkersAvailable: waiting"), NewCondition("ScalingUp", "True", "ScalingUp", "* MachineDeployment md: Scaling up from 0 to 1 replicas"), NewCondition("ControlPlaneInitialized", "False", "NotInitialized", "Control plane not yet initialized"), NewCondition("ControlPlaneAvailable", "False", "NotAvailable", "CP not available"), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: nil, // no readyReplicas field → no message }, { name: "ScalingUp from zero readyReplicas during creation", obj: makeClusterObj(1, 0, 0, 0, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "not available"), NewCondition("ScalingUp", "True", "ScalingUp", "* MachineDeployment md: Scaling up from 0 to 1 replicas"), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 0 to 1 machines"}, }, { name: "Desired workers exceed ready workers — reports scalingup before Available=False", obj: makeClusterObj(3, 3, 2, 2, 3), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "* MachineDeployment md: 2 available, 3 required"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("RollingOut", "False", "NotRollingOut", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 2 to 3 machines"}, }, // --- Priority 6: Available=False --- { name: "Available=False without any scaling → updating", obj: makeClusterObj(2, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", "* RemoteConnectionProbe: Remote connection not established yet"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"establishing connection to control plane"}, }, { name: "Available=False with empty message → updating with no messages", obj: makeClusterObj(1, 1, 1, 1, -1), conditions: []Condition{ NewCondition("Available", "False", "NotAvailable", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, }, { name: "Deleting=True takes priority over RollingOut=True on Cluster", obj: makeClusterObj(4, 5, 5, 5, 3), conditions: []Condition{ NewCondition("Deleting", "True", "WaitingForWorkersDeletion", "cleanup"), NewCondition("RollingOut", "True", "RollingOut", "* MachineDeployment md:\n * Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "scaling down"), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"cleanup"}, }, { name: "Paused=True takes priority over RollingOut=True on Cluster", obj: makeClusterObj(4, 5, 5, 5, 3), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "True", "Paused", ""), NewCondition("RollingOut", "True", "RollingOut", "* MachineDeployment md:\n * Rolling out 2 not up-to-date replicas"), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, { name: "RollingOut=False does not trigger rollingout — falls through to ScalingDown", obj: makeClusterObj(3, 4, 4, 4, -1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "False", "NotRollingOut", ""), NewCondition("ScalingDown", "True", "ScalingDown", "scaling down"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Available", "True", "Available", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 4 to 3 machines"}, }, { name: "RollingOut=True on Cluster with missing upToDateReplicas — no message", obj: makeClusterObj(4, 5, 5, 5, -1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "* MachineDeployment md:\n * Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "scaling down"), }, expectedState: "rollingout", expectedTransit: true, expectedError: false, expectedMessages: nil, }, // --- Priority 7: Steady state / pass through --- { name: "Steady state — all healthy → pass through", obj: makeClusterObj(2, 2, 2, 2, -1), conditions: []Condition{ NewCondition("Available", "True", "Available", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("WorkersAvailable", "True", "Available", ""), NewCondition("ControlPlaneAvailable", "True", "Available", ""), NewCondition("WorkerMachinesReady", "True", "Ready", ""), NewCondition("ControlPlaneMachinesReady", "True", "Ready", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Edge cases --- { name: "No conditions → pass through", obj: makeClusterObj(1, 1, 1, 1, -1), conditions: []Condition{}, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "Missing worker replica fields → pass through (no panic)", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, conditions: []Condition{ NewCondition("Available", "True", "Available", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "ScalingUp=True but no worker replica fields → updating, no message", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "True", "ScalingUp", "Scaling up"), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "ScalingDown=True but no worker replica fields → updating, no message", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "updating", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "Deleting=True takes priority over active scale-down", obj: makeClusterObj(1, 1, 1, 1, -1), conditions: []Condition{ NewCondition("Deleting", "True", "WaitingForWorkersDeletion", "cleanup in progress"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 1 to 0"), NewCondition("Available", "False", "NotAvailable", "not available"), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"cleanup in progress"}, }, { name: "Paused=True takes priority over scale-up detected by replicas", obj: makeClusterObj(3, 1, 1, 1, -1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "True", "Paused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := checkCAPIClusterTransitioning(tt.obj, tt.conditions, Summary{}) assert.Equal(t, tt.expectedState, result.State, "state mismatch") assert.Equal(t, tt.expectedTransit, result.Transitioning, "transitioning mismatch") assert.Equal(t, tt.expectedError, result.Error, "error mismatch") if tt.expectedMessages != nil { assert.Equal(t, tt.expectedMessages, result.Message, "messages mismatch") } else { assert.Empty(t, result.Message, "expected no messages") } }) } } // TestCheckTransitioning_CAPIClusterDispatch verifies that checkTransitioning // dispatches to checkCAPIClusterTransitioning for CAPI Clusters and not to // the generic path. func TestCheckTransitioning_CAPIClusterDispatch(t *testing.T) { // A CAPI Cluster with ScalingUp=True should get "updating" from the // CAPI Cluster path. capiObj := makeClusterObj(2, 1, 1, 1, -1) conditions := []Condition{ NewCondition("ScalingUp", "True", "ScalingUp", "* MachineDeployment md: Scaling up from 1 to 2 replicas"), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Available", "False", "NotAvailable", "something"), } result := checkTransitioning(capiObj, conditions, Summary{}) assert.Equal(t, "updating", result.State) assert.True(t, result.Transitioning) // A CAPI Cluster with RollingOut=True during a rolling upgrade should // get state="rollingout" instead of "updating" (from ScalingDown). rollingClusterObj := makeClusterObj(4, 5, 5, 5, 3) rollingConditions := []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "* MachineDeployment md:\n * Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "* MachineDeployment md: Scaling down from 4 to 3 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Available", "True", "Available", ""), } rollingResult := checkTransitioning(rollingClusterObj, rollingConditions, Summary{}) assert.Equal(t, "rollingout", rollingResult.State, "rolling upgrade should produce state=rollingout") assert.True(t, rollingResult.Transitioning) assert.Equal(t, []string{"rolling out 2 not up-to-date replicas"}, rollingResult.Message) // A non-CAPI Cluster (e.g. management.cattle.io) should use the generic path. genericObj := data.Object{ "apiVersion": "management.cattle.io/v3", "kind": "Cluster", } genericConditions := []Condition{ NewCondition("Available", "False", "MinimumReplicasUnavailable", "not available"), } genericResult := checkTransitioning(genericObj, genericConditions, Summary{}) assert.Equal(t, "updating", genericResult.State) assert.True(t, genericResult.Transitioning) } ================================================ FILE: pkg/summary/capi_machine_test.go ================================================ package summary import ( "testing" "github.com/rancher/wrangler/v3/pkg/data" "github.com/stretchr/testify/assert" ) func TestIsCAPIMachine(t *testing.T) { tests := []struct { name string obj data.Object expected bool }{ { name: "CAPI v1beta2 Machine", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Machine", }, expected: true, }, { name: "CAPI v1beta1 Machine", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta1", "kind": "Machine", }, expected: true, }, { name: "CAPI v1beta2 Cluster", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, expected: false, }, { name: "CAPI v1beta2 MachineSet", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, expected: false, }, { name: "non-CAPI Machine kind", obj: data.Object{ "apiVersion": "apps/v1", "kind": "Deployment", }, expected: false, }, { name: "empty object", obj: data.Object{}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, isCAPIMachine(tt.obj)) }) } } func TestParseMessage(t *testing.T) { tests := []struct { name string message string expectedDetail string expectedPrefix string }{ { name: "empty message", message: "", expectedDetail: "", expectedPrefix: "", }, { name: "single bullet BootstrapConfigReady", message: "* BootstrapConfigReady: RKEBootstrap status.initialization.dataSecretCreated is false", expectedDetail: "RKEBootstrap status.initialization.dataSecretCreated is false", expectedPrefix: "BootstrapConfigReady", }, { name: "single bullet InfrastructureReady", message: "* InfrastructureReady: creating server of kind (DigitaloceanMachine)", expectedDetail: "creating server of kind (DigitaloceanMachine)", expectedPrefix: "InfrastructureReady", }, { name: "single bullet NodeHealthy", message: "* NodeHealthy: Waiting for Cluster control plane to be initialized", expectedDetail: "Waiting for Cluster control plane to be initialized", expectedPrefix: "NodeHealthy", }, { name: "multiple bullets - takes first only", message: "* BootstrapConfigReady: bootstrap not ready\n* InfrastructureReady: infra not ready\n* NodeHealthy: waiting", expectedDetail: "bootstrap not ready", expectedPrefix: "BootstrapConfigReady", }, { name: "multiple bullets starting with InfrastructureReady", message: "* InfrastructureReady: creating server...\n* NodeHealthy: Waiting for CP init", expectedDetail: "creating server...", expectedPrefix: "InfrastructureReady", }, { name: "no bullet prefix", message: "some plain message", expectedDetail: "some plain message", expectedPrefix: "", }, { name: "bullet without colon separator", message: "* SomeCondition without colon", expectedDetail: "SomeCondition without colon", expectedPrefix: "", }, { name: "nested sub-bullet: NodeHealthy with empty inline detail", message: "* NodeHealthy:\n * Node.AllConditions: Kubelet stopped posting node status.", expectedDetail: "Kubelet stopped posting node status.", expectedPrefix: "NodeHealthy", }, { name: "nested sub-bullet: multiple sub-bullets takes first", message: "* NodeHealthy:\n * Node.AllConditions: first issue\n * Node.OtherCondition: second issue", expectedDetail: "first issue", expectedPrefix: "NodeHealthy", }, { name: "nested sub-bullet: sub-bullet without colon separator", message: "* NodeHealthy:\n * some plain sub-bullet", expectedDetail: "some plain sub-bullet", expectedPrefix: "NodeHealthy", }, { name: "nested sub-bullet: InfrastructureReady with empty inline detail", message: "* InfrastructureReady:\n * SubCondition: infra detail here", expectedDetail: "infra detail here", expectedPrefix: "InfrastructureReady", }, { name: "nested sub-bullet: no sub-bullets found (only non-bullet lines)", message: "* NodeHealthy:\n some non-bullet line", expectedDetail: "", expectedPrefix: "NodeHealthy", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { detail, prefix := parseMessage(tt.message) assert.Equal(t, tt.expectedDetail, detail) assert.Equal(t, tt.expectedPrefix, prefix) }) } } func TestCheckCAPIMachineTransitioning(t *testing.T) { tests := []struct { name string conditions []Condition expectedState string expectedTransit bool expectedError bool expectedMessages []string }{ // --- Priority 1: Deleting --- { name: "Deleting=True takes absolute priority", conditions: []Condition{ NewCondition("Reconciled", "Unknown", "Waiting", "reconciling something"), NewCondition("Deleting", "True", "Deleting", "machine is being deleted"), NewCondition("Ready", "False", "NotReady", "* InfrastructureReady: infra not ready"), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"machine is being deleted"}, }, { name: "Deleting=True with empty message", conditions: []Condition{ NewCondition("Deleting", "True", "Deleting", ""), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "Deleting=True with drain message rewrite", conditions: []Condition{ NewCondition("Deleting", "True", "Draining", "Drain not completed yet (1/3 nodes drained)"), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"Draining node"}, }, // --- Priority 2: Paused --- { name: "Paused=True takes priority over Reconciled and Ready", conditions: []Condition{ NewCondition("Reconciled", "Unknown", "Waiting", "reconciling"), NewCondition("Paused", "True", "Paused", "machine is paused"), NewCondition("Ready", "False", "NotReady", "* InfrastructureReady: infra not ready"), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, { name: "Paused=False is ignored (normal state)", conditions: []Condition{ NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Ready", "True", "Ready", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Priority 3: Reconciled not True --- { name: "Reconciled=Unknown → reconciling with message", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy: waiting"), NewCondition("Reconciled", "Unknown", "Waiting", "waiting for agent to check in and apply initial plan"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, expectedMessages: []string{"waiting for agent to check in and apply initial plan"}, }, { name: "Reconciled=Unknown with empty message", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy: waiting"), NewCondition("Reconciled", "Unknown", "Waiting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, }, { name: "Reconciled=False → reconciling with error", conditions: []Condition{ NewCondition("Ready", "True", "Ready", ""), NewCondition("Reconciled", "False", "ReconcileError", "failed to reconcile machine"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: false, expectedError: true, expectedMessages: []string{"failed to reconcile machine"}, }, { name: "Reconciled=False with empty message → reconciling with error, no messages", conditions: []Condition{ NewCondition("Ready", "True", "Ready", ""), NewCondition("Reconciled", "False", "ReconcileError", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: false, expectedError: true, expectedMessages: nil, }, { name: "Reconciled=Unknown takes priority over Ready=False", conditions: []Condition{ NewCondition("Ready", "False", "NotReady", "* InfrastructureReady: infra not ready"), NewCondition("Reconciled", "Unknown", "Waiting", "reconciling"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, expectedMessages: []string{"reconciling"}, }, // --- Priority 4: Ready=False --- { name: "Ready=False, first bullet BootstrapConfigReady → waitingforinfrastructure", conditions: []Condition{ NewCondition("Ready", "False", "NotReady", "* BootstrapConfigReady: RKEBootstrap status.initialization.dataSecretCreated is false\n"+ "* InfrastructureReady: DigitaloceanMachine status.initialization.provisioned is false\n"+ "* NodeHealthy: Waiting for Cluster control plane to be initialized"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "waitingforinfrastructure", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "Ready=False, first bullet InfrastructureReady → waitingfornoderef", conditions: []Condition{ NewCondition("Ready", "False", "NotReady", "* InfrastructureReady: creating server [fleet-default/prod-jiaqi-pa-qxwqc-9gdk2] of kind (DigitaloceanMachine) for machine prod-jiaqi-pa-qxwqc-9gdk2 in infrastructure provider\n"+ "* NodeHealthy: Waiting for Cluster control plane to be initialized"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "waitingfornoderef", expectedTransit: true, expectedError: false, expectedMessages: []string{"creating server [fleet-default/prod-jiaqi-pa-qxwqc-9gdk2] of kind (DigitaloceanMachine) for machine prod-jiaqi-pa-qxwqc-9gdk2 in infrastructure provider"}, }, { name: "Ready=False, unknown first bullet → pass through (no state set)", conditions: []Condition{ NewCondition("Ready", "False", "NotReady", "* SomeOtherCondition: something happened"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "Ready=False, InfrastructureReady detail suppressed when ending with 'status.initialization.provisioned is false'", conditions: []Condition{ NewCondition("Ready", "False", "NotReady", "* InfrastructureReady: DigitaloceanMachine status.initialization.provisioned is false\n"+ "* NodeHealthy: Waiting for Cluster control plane to be initialized"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "waitingfornoderef", expectedTransit: true, expectedError: false, // Detail is suppressed — no messages expected. }, { name: "Ready=False with empty message → pass through", conditions: []Condition{ NewCondition("Ready", "False", "NotReady", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Priority 5: Ready=Unknown --- { name: "Ready=Unknown with NodeHealthy bullet → reconciling", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy: Waiting for Cluster control plane to be initialized"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, expectedMessages: []string{"Waiting for Cluster control plane to be initialized"}, }, { name: "Ready=Unknown, Reconciled=True → reconciling (rule 5, not rule 3)", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy: Waiting for Cluster control plane to be initialized"), NewCondition("Reconciled", "True", "Reconciled", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, expectedMessages: []string{"Waiting for Cluster control plane to be initialized"}, }, { name: "Ready=Unknown with multiple bullets → reconciling with first bullet detail", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* InfrastructureReady: some issue\n* NodeHealthy: waiting"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, expectedMessages: []string{"some issue"}, }, { name: "Ready=Unknown with nested sub-bullet (NodeHealthy empty detail) → reconciling with sub-bullet detail", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy:\n * Node.AllConditions: Kubelet stopped posting node status."), NewCondition("Reconciled", "True", "Reconciled", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, expectedMessages: []string{"Kubelet stopped posting node status."}, }, { name: "Ready=Unknown with NodeHealthy detail suppressed when ending with 'to report spec.providerID'", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy: Waiting for Node controller to report spec.providerID"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, // Detail is suppressed because it ends with "to report spec.providerID". }, { name: "Ready=Unknown with empty message → reconciling with no messages", conditions: []Condition{ NewCondition("Ready", "Unknown", "ReadyUnknown", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "reconciling", expectedTransit: true, expectedError: false, }, // --- Priority 6: Ready=True --- { name: "Ready=True → pass through (no state set)", conditions: []Condition{ NewCondition("Ready", "True", "Ready", ""), NewCondition("Reconciled", "True", "Reconciled", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Edge cases --- { name: "no conditions → pass through", conditions: []Condition{}, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "no Ready or Reconciled conditions → pass through", conditions: []Condition{ NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "Reconciled=True is skipped (not a problem state)", conditions: []Condition{ NewCondition("Ready", "True", "Ready", ""), NewCondition("Reconciled", "True", "Reconciled", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "Deleting=False does not trigger removing", conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Ready", "True", "Ready", ""), NewCondition("Reconciled", "True", "Reconciled", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := checkCAPIMachineTransitioning(tt.conditions, Summary{}) assert.Equal(t, tt.expectedState, result.State) assert.Equal(t, tt.expectedTransit, result.Transitioning) assert.Equal(t, tt.expectedError, result.Error) if tt.expectedMessages != nil { assert.Equal(t, tt.expectedMessages, result.Message) } else { assert.Empty(t, result.Message) } }) } } // TestCheckTransitioning_CAPIMachineDispatch verifies that checkTransitioning // dispatches to checkCAPIMachineTransitioning for CAPI Machines and to // checkGenericTransitioning for everything else. func TestCheckTransitioning_CAPIMachineDispatch(t *testing.T) { // A CAPI Machine with Reconciled=Unknown should get "reconciling" from // the CAPI path, not the generic TransitioningUnknown path. capiObj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Machine", } conditions := []Condition{ NewCondition("Reconciled", "Unknown", "Waiting", "waiting for agent"), NewCondition("Ready", "Unknown", "ReadyUnknown", "* NodeHealthy: waiting"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), } result := checkTransitioning(capiObj, conditions, Summary{}) assert.Equal(t, "reconciling", result.State) assert.True(t, result.Transitioning) // A non-CAPI object with Reconciled=Unknown should use the generic path. // In TransitioningUnknown, Reconciled maps to "reconciling" for Unknown status. genericObj := data.Object{ "apiVersion": "management.cattle.io/v3", "kind": "Cluster", } genericConditions := []Condition{ NewCondition("Reconciled", "Unknown", "Waiting", "waiting for something"), } genericResult := checkTransitioning(genericObj, genericConditions, Summary{}) assert.Equal(t, "reconciling", genericResult.State) assert.True(t, genericResult.Transitioning) } // TestCheckTransitioning_NonCAPIMachineUnchanged verifies that the generic // path remains unchanged for non-CAPI objects. func TestCheckTransitioning_NonCAPIMachineUnchanged(t *testing.T) { obj := data.Object{ "apiVersion": "apps/v1", "kind": "Deployment", } // Available=False in TransitioningFalse maps to "updating" conditions := []Condition{ NewCondition("Available", "False", "MinimumReplicasUnavailable", "Deployment does not have minimum availability"), } result := checkTransitioning(obj, conditions, Summary{}) assert.Equal(t, "updating", result.State) assert.True(t, result.Transitioning) assert.False(t, result.Error) } ================================================ FILE: pkg/summary/capi_machineset_test.go ================================================ package summary import ( "testing" "github.com/rancher/wrangler/v3/pkg/data" "github.com/stretchr/testify/assert" ) func TestIsCAPIMachineSet(t *testing.T) { tests := []struct { name string obj data.Object expected bool }{ { name: "CAPI v1beta2 MachineSet", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, expected: true, }, { name: "CAPI v1beta1 MachineSet", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta1", "kind": "MachineSet", }, expected: true, }, { name: "CAPI v1beta2 Machine (not MachineSet)", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Machine", }, expected: false, }, { name: "CAPI v1beta2 Cluster", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, expected: false, }, { name: "non-CAPI MachineSet kind", obj: data.Object{ "apiVersion": "apps/v1", "kind": "ReplicaSet", }, expected: false, }, { name: "empty object", obj: data.Object{}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, isCAPIMachineSet(tt.obj)) }) } } // makeMachineSetObj builds a minimal CAPI MachineSet data.Object with the // given replica fields. Use -1 to omit a field (simulating it not being set). func makeMachineSetObj(specReplicas, statusReplicas, readyReplicas int64) data.Object { obj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", "spec": map[string]interface{}{}, "status": map[string]interface{}{}, } if specReplicas >= 0 { obj["spec"].(map[string]interface{})["replicas"] = specReplicas } if statusReplicas >= 0 { obj["status"].(map[string]interface{})["replicas"] = statusReplicas } if readyReplicas >= 0 { obj["status"].(map[string]interface{})["readyReplicas"] = readyReplicas } return obj } // makeMachineDeploymentObj builds a minimal CAPI MachineDeployment data.Object // with the given replica fields. Use -1 to omit a field. func makeMachineDeploymentObj(specReplicas, statusReplicas, readyReplicas, upToDateReplicas int64) data.Object { obj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineDeployment", "spec": map[string]interface{}{}, "status": map[string]interface{}{}, } if specReplicas >= 0 { obj["spec"].(map[string]interface{})["replicas"] = specReplicas } if statusReplicas >= 0 { obj["status"].(map[string]interface{})["replicas"] = statusReplicas } if readyReplicas >= 0 { obj["status"].(map[string]interface{})["readyReplicas"] = readyReplicas } if upToDateReplicas >= 0 { obj["status"].(map[string]interface{})["upToDateReplicas"] = upToDateReplicas } return obj } func TestCheckCAPIMachineSetAndDeploymentTransitioning(t *testing.T) { tests := []struct { name string obj data.Object conditions []Condition expectedState string expectedTransit bool expectedError bool expectedMessages []string }{ // --- Priority 1: Deleting --- { name: "Deleting=True takes absolute priority", obj: makeMachineSetObj(2, 2, 2), conditions: []Condition{ NewCondition("Deleting", "True", "Deleting", "machineset is being deleted"), NewCondition("ScalingUp", "True", "ScalingUp", "Scaling up from 1 to 2 replicas"), NewCondition("Paused", "False", "NotPaused", ""), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"machineset is being deleted"}, }, { name: "Deleting=True with empty message", obj: makeMachineSetObj(2, 2, 2), conditions: []Condition{ NewCondition("Deleting", "True", "Deleting", ""), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "Deleting=False is ignored", obj: makeMachineSetObj(2, 2, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("MachinesReady", "True", "Ready", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Priority 2: Paused --- { name: "Paused=True takes priority over scaling conditions", obj: makeMachineSetObj(2, 1, 1), conditions: []Condition{ NewCondition("Paused", "True", "Paused", "machineset is paused"), NewCondition("ScalingUp", "True", "ScalingUp", "Scaling up from 1 to 2 replicas"), NewCondition("Deleting", "False", "NotDeleting", ""), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, { name: "Paused=False is ignored", obj: makeMachineSetObj(2, 2, 2), conditions: []Condition{ NewCondition("Paused", "False", "NotPaused", ""), NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("MachinesReady", "True", "Ready", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Priority 3: RollingOut --- // RollingOut only applies to MachineDeployment, not MachineSet. { name: "RollingOut=True on MachineDeployment takes priority over ScalingDown", obj: makeMachineDeploymentObj(3, 4, 3, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "Rolling out 2 not up-to-date replicas\n* DigitaloceanMachine is not up-to-date"), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 4 to 3 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "rollingout", expectedTransit: true, expectedError: false, expectedMessages: []string{"rolling out 2 not up-to-date replicas"}, }, { name: "RollingOut=True on MachineDeployment takes priority over ScalingUp by replicas", obj: makeMachineDeploymentObj(3, 3, 1, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "rollingout", expectedTransit: true, expectedError: false, expectedMessages: []string{"rolling out 2 not up-to-date replicas"}, }, // --- Priority 4: ScalingDown --- { name: "ScalingDown=True — message always constructed from replicas", obj: makeMachineSetObj(1, 2, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 2 to 1 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 2 to 1 replicas, waiting for machines to be deleted"}, }, { name: "ScalingDown=True with empty condition message — constructs from replicas", obj: makeMachineSetObj(1, 2, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 2 to 1 replicas, waiting for machines to be deleted"}, }, { name: "ScalingDown detected by replica mismatch only (stale condition)", obj: makeMachineSetObj(1, 2, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 2 to 1 replicas, waiting for machines to be deleted"}, }, { name: "ScalingDown detected by replica mismatch", obj: makeMachineSetObj(1, 2, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("MachinesReady", "True", "Ready", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 2 to 1 replicas, waiting for machines to be deleted"}, }, { name: "ScalingDown takes priority over ScalingUp when both detectable", obj: makeMachineSetObj(1, 3, 0), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 3 to 1 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 3 to 1 replicas, waiting for machines to be deleted"}, }, // --- Priority 5: ScalingUp --- { name: "ScalingUp=True — message always constructed from replicas", obj: makeMachineSetObj(2, 1, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "True", "ScalingUp", "Scaling up from 1 to 2 replicas"), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 1 to 2 replicas, waiting for machines to be ready"}, }, { name: "ScalingUp=True with empty condition message — constructs from replicas", obj: makeMachineSetObj(2, 1, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "True", "ScalingUp", ""), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 1 to 2 replicas, waiting for machines to be ready"}, }, { name: "ScalingUp detected by readyReplicas mismatch only", obj: makeMachineSetObj(2, 1, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 1 to 2 replicas, waiting for machines to be ready"}, }, { name: "Spec replicas exceed ready replicas — reports scalingup even when all machines exist", obj: makeMachineSetObj(2, 2, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("MachinesReady", "True", "Ready", ""), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 1 to 2 replicas, waiting for machines to be ready"}, }, { name: "MachineDeployment with ready replicas below spec — reports scalingup before other conditions", obj: makeMachineDeploymentObj(3, 3, 2, 3), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "False", "NotRollingOut", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("Available", "False", "NotAvailable", "2 available replicas, at least 3 required"), NewCondition("MachinesReady", "Unknown", "ReadyUnknown", "* Machine test-m:\n * NodeHealthy: Kubelet stopped posting node status"), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 2 to 3 replicas, waiting for machines to be ready"}, }, { name: "Deleting=True takes priority over RollingOut=True", obj: makeMachineDeploymentObj(3, 4, 3, 2), conditions: []Condition{ NewCondition("Deleting", "True", "Deleting", "being deleted"), NewCondition("RollingOut", "True", "RollingOut", "Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 4 to 3 replicas"), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"being deleted"}, }, { name: "Paused=True takes priority over RollingOut=True", obj: makeMachineDeploymentObj(3, 4, 3, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "True", "Paused", ""), NewCondition("RollingOut", "True", "RollingOut", "Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 4 to 3 replicas"), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, { name: "RollingOut=False does not trigger rollingout — falls through to ScalingDown", obj: makeMachineDeploymentObj(1, 2, 2, -1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "False", "NotRollingOut", ""), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 2 to 1 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling down from 2 to 1 replicas, waiting for machines to be deleted"}, }, { name: "RollingOut=True on MachineDeployment with missing upToDateReplicas — no message", obj: makeMachineDeploymentObj(3, 4, 3, -1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 4 to 3 replicas"), }, expectedState: "rollingout", expectedTransit: true, expectedError: false, expectedMessages: nil, }, // --- Pass through (steady state) --- { name: "Steady state — all replicas match → pass through", obj: makeMachineSetObj(2, 2, 2), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("MachinesReady", "True", "Ready", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, // --- Edge cases --- { name: "No conditions → pass through", obj: makeMachineSetObj(1, 1, 1), conditions: []Condition{}, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "Missing replica fields → pass through (no panic)", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "", expectedTransit: false, expectedError: false, }, { name: "ScalingUp=True but no replica fields → scalingup, no message", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "True", "ScalingUp", ""), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: nil, }, { name: "ScalingUp from zero readyReplicas", obj: makeMachineSetObj(2, 0, 0), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "True", "ScalingUp", ""), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 0 to 2 replicas, waiting for machines to be ready"}, }, { name: "ScalingUp with larger replica counts (3 → 5)", obj: makeMachineSetObj(5, 3, 3), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "True", "ScalingUp", ""), }, expectedState: "scalingup", expectedTransit: true, expectedError: false, expectedMessages: []string{"Scaling up from 3 to 5 replicas, waiting for machines to be ready"}, }, { name: "Deleting=True takes priority over active scale-down", obj: makeMachineSetObj(1, 2, 2), conditions: []Condition{ NewCondition("Deleting", "True", "Deleting", "machineset cleanup in progress"), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 2 to 1 replicas"), }, expectedState: "deleting", expectedTransit: true, expectedError: false, expectedMessages: []string{"machineset cleanup in progress"}, }, { name: "Paused=True takes priority over scale-up detected by replicas", obj: makeMachineSetObj(3, 1, 1), conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "True", "Paused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "paused", expectedTransit: true, expectedError: false, }, { name: "ScalingDown=True but no replica fields → scalingdown, no message", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, conditions: []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "True", "ScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), }, expectedState: "scalingdown", expectedTransit: true, expectedError: false, expectedMessages: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := checkCAPIMachineSetAndDeploymentTransitioning(tt.obj, tt.conditions, Summary{}) assert.Equal(t, tt.expectedState, result.State, "state mismatch") assert.Equal(t, tt.expectedTransit, result.Transitioning, "transitioning mismatch") assert.Equal(t, tt.expectedError, result.Error, "error mismatch") if tt.expectedMessages != nil { assert.Equal(t, tt.expectedMessages, result.Message, "messages mismatch") } else { assert.Empty(t, result.Message, "expected no messages") } }) } } func TestIsCAPIMachineDeployment(t *testing.T) { tests := []struct { name string obj data.Object expected bool }{ { name: "CAPI v1beta2 MachineDeployment", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineDeployment", }, expected: true, }, { name: "CAPI v1beta1 MachineDeployment", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta1", "kind": "MachineDeployment", }, expected: true, }, { name: "CAPI v1beta2 MachineSet (not MachineDeployment)", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineSet", }, expected: false, }, { name: "CAPI v1beta2 Cluster", obj: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", }, expected: false, }, { name: "non-CAPI Deployment kind", obj: data.Object{ "apiVersion": "apps/v1", "kind": "Deployment", }, expected: false, }, { name: "empty object", obj: data.Object{}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, isCAPIMachineDeployment(tt.obj)) }) } } // TestCheckTransitioning_CAPIMachineDeploymentDispatch verifies that // checkTransitioning dispatches CAPI MachineDeployment objects to // checkCAPIMachineSetAndDeploymentTransitioning and that the replica-field // paths behave correctly for deployments (not just MachineSets). func TestCheckTransitioning_CAPIMachineDeploymentDispatch(t *testing.T) { // A MachineDeployment scaling up: spec.replicas=3, status.replicas=1. // Even with ScalingUp=False (stale condition), the replica mismatch // (spec > status) should be detected via the CAPI handler. scalingObj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineDeployment", "spec": map[string]interface{}{ "replicas": int64(3), }, "status": map[string]interface{}{ "replicas": int64(1), "readyReplicas": int64(1), }, } conditions := []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), } result := checkTransitioning(scalingObj, conditions, Summary{}) assert.Equal(t, "scalingup", result.State) assert.True(t, result.Transitioning) assert.Equal(t, []string{"Scaling up from 1 to 3 replicas, waiting for machines to be ready"}, result.Message) // A MachineDeployment in steady state: all replicas match. steadyObj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineDeployment", "spec": map[string]interface{}{ "replicas": int64(2), }, "status": map[string]interface{}{ "replicas": int64(2), "readyReplicas": int64(2), }, } steadyConditions := []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("ScalingDown", "False", "NotScalingDown", ""), NewCondition("ScalingUp", "False", "NotScalingUp", ""), NewCondition("MachinesReady", "True", "Ready", ""), } steadyResult := checkTransitioning(steadyObj, steadyConditions, Summary{}) assert.Empty(t, steadyResult.State, "steady-state MachineDeployment should pass through") assert.False(t, steadyResult.Transitioning) assert.False(t, steadyResult.Error) // A MachineDeployment doing a rolling upgrade: RollingOut=True should // take priority over the ScalingDown condition and replica mismatch. rollingObj := data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "MachineDeployment", "spec": map[string]interface{}{ "replicas": int64(3), }, "status": map[string]interface{}{ "replicas": int64(4), "readyReplicas": int64(3), "upToDateReplicas": int64(2), }, } rollingConditions := []Condition{ NewCondition("Deleting", "False", "NotDeleting", ""), NewCondition("Paused", "False", "NotPaused", ""), NewCondition("RollingOut", "True", "RollingOut", "Rolling out 2 not up-to-date replicas"), NewCondition("ScalingDown", "True", "ScalingDown", "Scaling down from 4 to 3 replicas"), NewCondition("ScalingUp", "False", "NotScalingUp", ""), } rollingResult := checkTransitioning(rollingObj, rollingConditions, Summary{}) assert.Equal(t, "rollingout", rollingResult.State, "rolling upgrade should produce state=rollingout") assert.True(t, rollingResult.Transitioning) assert.Equal(t, []string{"rolling out 2 not up-to-date replicas"}, rollingResult.Message) } ================================================ FILE: pkg/summary/cattletypes.go ================================================ package summary import ( "strings" "github.com/rancher/wrangler/v3/pkg/data" ) func checkCattleReady(obj data.Object, condition []Condition, summary Summary) Summary { if strings.Contains(obj.String("apiVersion"), "cattle.io/") { for _, condition := range condition { if condition.Type() == "Ready" && condition.Status() == "False" && condition.Message() != "" { summary.Message = append(summary.Message, condition.Message()) summary.Error = true return summary } } } return summary } func checkCattleTypes(obj data.Object, condition []Condition, summary Summary) Summary { return checkRelease(obj, condition, summary) } func checkRelease(obj data.Object, _ []Condition, summary Summary) Summary { if !isKind(obj, "App", "catalog.cattle.io") { return summary } if obj.String("status", "summary", "state") != "deployed" { return summary } for _, resources := range obj.Slice("spec", "resources") { summary.Relationships = append(summary.Relationships, Relationship{ Name: resources.String("name"), Namespace: resources.String("namespace"), Kind: resources.String("kind"), APIVersion: resources.String("apiVersion"), Type: "helmresource", }) } return summary } ================================================ FILE: pkg/summary/cattletypes_test.go ================================================ package summary import ( "testing" "github.com/rancher/wrangler/v3/pkg/data" "github.com/stretchr/testify/assert" ) func TestCheckRelease(t *testing.T) { type testCase struct { name string input data.Object expected Summary } cases := []testCase{ { name: "namespaced resource for deployed app.catalog.cattle.io", input: data.Object{ "apiVersion": "catalog.cattle.io/v1", "kind": "App", "spec": map[string]any{ "resources": []any{ map[string]any{ "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role", "name": "monitoring-dashboard-admin", "namespace": "cattle-dashboards", }, }, }, "status": map[string]any{ "summary": map[string]any{ "state": "deployed", }, }, }, expected: Summary{ Relationships: []Relationship{ { Name: "monitoring-dashboard-admin", Namespace: "cattle-dashboards", Kind: "Role", APIVersion: "rbac.authorization.k8s.io/v1", Type: "helmresource", }, }, }, }, { name: "non-namespaced resource for deployed app.catalog.cattle.io", input: data.Object{ "apiVersion": "catalog.cattle.io/v1", "kind": "App", "spec": map[string]any{ "resources": []any{ map[string]any{ "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "ClusterRole", "name": "monitoring-admin", }, }, }, "status": map[string]any{ "summary": map[string]any{ "state": "deployed", }, }, }, expected: Summary{ Relationships: []Relationship{ { Name: "monitoring-admin", Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1", Type: "helmresource", }, }, }, }, { name: "resource for undeployed app.catalog.cattle.io", input: data.Object{ "apiVersion": "catalog.cattle.io/v1", "kind": "App", "spec": map[string]any{ "resources": []any{ map[string]any{ "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role", "name": "monitoring-dashboard-admin", "namespace": "cattle-dashboards", }, }, }, "status": map[string]any{ "summary": map[string]any{ "state": "failed", }, }, }, expected: Summary{}, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { actual := checkRelease(c.input, nil, Summary{}) assert.Equal(t, c.expected, actual) }) } } ================================================ FILE: pkg/summary/client/interface.go ================================================ package client import ( "context" "github.com/rancher/wrangler/v3/pkg/summary" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" ) type Interface interface { Resource(resource schema.GroupVersionResource) NamespaceableResourceInterface } type ExtendedInterface interface { Interface ResourceWithOptions(resource schema.GroupVersionResource, opts *Options) NamespaceableResourceInterface } type ResourceInterface interface { List(ctx context.Context, opts metav1.ListOptions) (*summary.SummarizedObjectList, error) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) } type NamespaceableResourceInterface interface { Namespace(string) ResourceInterface ResourceInterface } ================================================ FILE: pkg/summary/client/options.go ================================================ package client import ( "github.com/rancher/wrangler/v3/pkg/schemas" ) type Options struct { Schema *schemas.Schema } ================================================ FILE: pkg/summary/client/simple.go ================================================ package client import ( "context" "github.com/rancher/wrangler/v3/pkg/schemas" "github.com/rancher/wrangler/v3/pkg/summary" metav1 "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/watch" "k8s.io/client-go/dynamic" ) type summaryClient struct { client dynamic.Interface } var _ Interface = &summaryClient{} func NewForDynamicClient(client dynamic.Interface) Interface { return &summaryClient{client: client} } func NewForExtendedDynamicClient(client dynamic.Interface) ExtendedInterface { return &summaryClient{client: client} } type summaryResourceClient struct { client dynamic.Interface namespace string resource schema.GroupVersionResource options Options } func (c *summaryClient) Resource(resource schema.GroupVersionResource) NamespaceableResourceInterface { return c.ResourceWithOptions(resource, nil) } func (c *summaryClient) ResourceWithOptions(resource schema.GroupVersionResource, opts *Options) NamespaceableResourceInterface { if opts == nil { opts = &Options{} } return &summaryResourceClient{ client: c.client, resource: resource, options: *opts, } } func (c *summaryResourceClient) Namespace(ns string) ResourceInterface { ret := *c ret.namespace = ns return &ret } func (c *summaryResourceClient) List(ctx context.Context, opts metav1.ListOptions) (*summary.SummarizedObjectList, error) { var ( u *unstructured.UnstructuredList err error ) if c.namespace == "" { u, err = c.client.Resource(c.resource).List(ctx, opts) } else { u, err = c.client.Resource(c.resource).Namespace(c.namespace).List(ctx, opts) } if err != nil { return nil, err } list := &summary.SummarizedObjectList{ TypeMeta: metav1.TypeMeta{ Kind: u.GetKind(), APIVersion: u.GetAPIVersion(), }, ListMeta: metav1.ListMeta{ ResourceVersion: u.GetResourceVersion(), Continue: u.GetContinue(), RemainingItemCount: u.GetRemainingItemCount(), }, } for _, obj := range u.Items { list.Items = append(list.Items, *summary.SummarizedWithOptions(&obj, generateSummarizeOpts(c.options.Schema))) } return list, nil } func (c *summaryResourceClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { var ( resp watch.Interface err error ) eventChan := make(chan watch.Event) if c.namespace == "" { resp, err = c.client.Resource(c.resource).Watch(ctx, opts) } else { resp, err = c.client.Resource(c.resource).Namespace(c.namespace).Watch(ctx, opts) } if err != nil { return nil, err } go func() { defer close(eventChan) for event := range resp.ResultChan() { // don't encode status objects if _, ok := event.Object.(*metav1.Status); !ok { event.Object = summary.SummarizedWithOptions(event.Object, generateSummarizeOpts(c.options.Schema)) } eventChan <- event } }() return &watcher{ Interface: resp, eventChan: eventChan, }, nil } type watcher struct { watch.Interface eventChan chan watch.Event } func (w watcher) ResultChan() <-chan watch.Event { return w.eventChan } func generateSummarizeOpts(schema *schemas.Schema) *summary.SummarizeOptions { hasObservedGeneration := false if schema != nil && schema.Attributes != nil { if v, ok := schema.Attributes["hasObservedGeneration"]; ok { if b, ok := v.(bool); ok { hasObservedGeneration = b } } } summaryOpts := &summary.SummarizeOptions{ HasObservedGeneration: hasObservedGeneration, } return summaryOpts } ================================================ FILE: pkg/summary/condition.go ================================================ package summary import ( "encoding/json" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "github.com/rancher/wrangler/v3/pkg/data" ) func GetUnstructuredConditions(obj map[string]interface{}) []Condition { return getConditions(obj) } func getRawConditions(obj data.Object) []data.Object { result := make([]data.Object, 0) conditions := obj.Slice("status", "conditions") if len(conditions) != 0 { result = append(result, conditions...) } annoConditions := getAnnotationConditions(obj) if len(annoConditions) != 0 { result = append(result, annoConditions...) } return result } // getAnnotationConditions extracts conditions from the cattle.io/status annotation. // Returns an empty slice if the annotation doesn't exist or cannot be parsed. func getAnnotationConditions(obj data.Object) []data.Object { statusAnn := obj.String("metadata", "annotations", "cattle.io/status") if statusAnn == "" { return []data.Object{} } var status data.Object if err := json.Unmarshal([]byte(statusAnn), &status); err != nil { return []data.Object{} } conditions := status.Slice("conditions") if conditions == nil { return []data.Object{} } return conditions } func getConditions(obj data.Object) (result []Condition) { for _, condition := range getRawConditions(obj) { result = append(result, Condition{Object: condition}) } return } type Condition struct { data.Object } func NewCondition(conditionType, status, reason, message string) Condition { return Condition{ Object: map[string]interface{}{ "type": conditionType, "status": status, "reason": reason, "message": message, }, } } func (c Condition) Type() string { return c.String("type") } func (c Condition) Status() string { return c.String("status") } func (c Condition) Reason() string { return c.String("reason") } func (c Condition) Message() string { return c.String("message") } func (c Condition) Equals(other Condition) bool { return c.Type() == other.Type() && c.Status() == other.Status() && c.Reason() == other.Reason() && c.Message() == other.Message() } func NormalizeConditions(runtimeObj runtime.Object) { if runtimeObj == nil { return } unstr, ok := runtimeObj.(*unstructured.Unstructured) if !ok { return } obj := data.Object(unstr.Object) if conditions := obj.Slice("status", "conditions"); len(conditions) > 0 { normalizeAndSetConditions(obj, conditions, "status", "conditions") } } func normalizeAndSetConditions(obj data.Object, conditions []data.Object, path ...string) { var newConditions []interface{} for _, condition := range conditions { var summary Summary for _, summarizer := range ConditionSummarizers { summary = summarizer(obj, []Condition{{Object: condition}}, summary) } condition.Set("error", summary.Error) condition.Set("transitioning", summary.Transitioning) if condition.String("lastUpdateTime") == "" { condition.Set("lastUpdateTime", condition.String("lastTransitionTime")) } newConditions = append(newConditions, map[string]interface{}(condition)) } if len(newConditions) > 0 { obj.SetNested(newConditions, path...) } } ================================================ FILE: pkg/summary/condition_test.go ================================================ package summary import ( "testing" "github.com/rancher/wrangler/v3/pkg/data" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestGetRawConditions(t *testing.T) { tests := []struct { name string input data.Object expected []data.Object }{ { name: "standard conditions in status field", input: data.Object{ "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, map[string]interface{}{ "type": "Available", "status": "False", }, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, { "type": "Available", "status": "False", }, }, }, { name: "annotation conditions are appended to status conditions", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[{"type":"Annotation","status":"True"}]}`, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, { "type": "Annotation", "status": "True", }, }, }, { name: "annotation conditions with CAPI v1beta2 standard conditions", input: data.Object{ "apiVersion": "cluster.x-k8s.io/v1beta2", "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[{"type":"Annotation","status":"True"}]}`, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, { "type": "Annotation", "status": "True", }, }, }, { name: "only annotation conditions when status field is empty", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[{"type":"Annotation","status":"True"}]}`, }, }, }, expected: []data.Object{ { "type": "Annotation", "status": "True", }, }, }, { name: "no conditions at all", input: data.Object{}, expected: []data.Object{}, }, { name: "invalid annotation JSON is ignored", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `invalid json`, }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, }, }, { name: "empty annotation string is ignored", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": "", }, }, "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getRawConditions(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestGetAnnotationConditions(t *testing.T) { tests := []struct { name string input data.Object expected []data.Object }{ { name: "parses valid annotation conditions", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[{"type":"Ready","status":"True"}]}`, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, }, }, { name: "parses multiple annotation conditions", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[{"type":"Ready","status":"True"},{"type":"Available","status":"False"}]}`, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", }, { "type": "Available", "status": "False", }, }, }, { name: "returns empty slice when annotation is empty string", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": "", }, }, }, expected: []data.Object{}, }, { name: "returns empty slice when annotation doesn't exist", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{}, }, }, expected: []data.Object{}, }, { name: "returns empty slice when metadata doesn't exist", input: data.Object{}, expected: []data.Object{}, }, { name: "returns empty slice when JSON is invalid", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{invalid json}`, }, }, }, expected: []data.Object{}, }, { name: "returns empty slice when JSON is valid but has no conditions", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"other":"field"}`, }, }, }, expected: []data.Object{}, }, { name: "returns empty slice when conditions array is empty", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[]}`, }, }, }, expected: []data.Object{}, }, { name: "handles complex condition objects", input: data.Object{ "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ "cattle.io/status": `{"conditions":[{"type":"Ready","status":"True","reason":"AllGood","message":"Everything is fine","lastTransitionTime":"2024-01-01T00:00:00Z"}]}`, }, }, }, expected: []data.Object{ { "type": "Ready", "status": "True", "reason": "AllGood", "message": "Everything is fine", "lastTransitionTime": "2024-01-01T00:00:00Z", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getAnnotationConditions(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestNormalizeConditions(t *testing.T) { tests := []struct { name string input *unstructured.Unstructured expectedStatusConditions []interface{} }{ { name: "normalizes standard status conditions", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Available", "status": "True", "lastTransitionTime": "2024-01-01T00:00:00Z", }, }, }, }, }, expectedStatusConditions: []interface{}{ map[string]interface{}{ "type": "Available", "status": "True", "lastTransitionTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-01T00:00:00Z", "error": false, "transitioning": false, }, }, }, { name: "normalizes CAPI v1beta2 standard conditions", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "cluster.x-k8s.io/v1beta2", "kind": "Cluster", "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Available", "status": "True", "lastTransitionTime": "2024-01-02T00:00:00Z", }, }, }, }, }, expectedStatusConditions: []interface{}{ map[string]interface{}{ "type": "Available", "status": "True", "lastTransitionTime": "2024-01-02T00:00:00Z", "lastUpdateTime": "2024-01-02T00:00:00Z", "error": false, "transitioning": false, }, }, }, { name: "preserves lastUpdateTime if already set", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Available", "status": "True", "lastTransitionTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-02T00:00:00Z", }, }, }, }, }, expectedStatusConditions: []interface{}{ map[string]interface{}{ "type": "Available", "status": "True", "lastTransitionTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-02T00:00:00Z", "error": false, "transitioning": false, }, }, }, { name: "handles empty status", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", }, }, expectedStatusConditions: nil, }, { name: "sets transitioning for False status condition", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "status": map[string]interface{}{ "conditions": []interface{}{ map[string]interface{}{ "type": "Available", "status": "False", "reason": "MinimumReplicasUnavailable", "message": "Deployment does not have minimum availability", "lastTransitionTime": "2024-01-01T00:00:00Z", }, }, }, }, }, expectedStatusConditions: []interface{}{ map[string]interface{}{ "type": "Available", "status": "False", "reason": "MinimumReplicasUnavailable", "message": "Deployment does not have minimum availability", "lastTransitionTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-01T00:00:00Z", "error": false, "transitioning": true, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { NormalizeConditions(tt.input) // Check standard status conditions statusConditions, _, _ := unstructured.NestedSlice(tt.input.Object, "status", "conditions") if tt.expectedStatusConditions == nil { assert.Nil(t, statusConditions) } else { assert.Equal(t, tt.expectedStatusConditions, statusConditions) } }) } } func TestNormalizeConditionsNonUnstructured(t *testing.T) { // Test that NormalizeConditions handles non-unstructured objects gracefully // Passing nil should not panic because the type assertion will fail NormalizeConditions(nil) // Should not panic } ================================================ FILE: pkg/summary/coretypes.go ================================================ package summary import ( "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func checkHasPodTemplate(obj data.Object, condition []Condition, summary Summary) Summary { template := obj.Map("spec", "template") if template == nil { return summary } if !isKind(obj, "ReplicaSet", "apps/", "extension/") && !isKind(obj, "DaemonSet", "apps/", "extension/") && !isKind(obj, "StatefulSet", "apps/", "extension/") && !isKind(obj, "Deployment", "apps/", "extension/") && !isKind(obj, "Job", "batch/") && !isKind(obj, "Service") { return summary } return checkPodTemplate(template, condition, summary) } func checkHasPodSelector(obj data.Object, condition []Condition, summary Summary) Summary { selector := obj.Map("spec", "selector") if selector == nil { return summary } if !isKind(obj, "ReplicaSet", "apps/", "extension/") && !isKind(obj, "DaemonSet", "apps/", "extension/") && !isKind(obj, "StatefulSet", "apps/", "extension/") && !isKind(obj, "Deployment", "apps/", "extension/") && !isKind(obj, "Job", "batch/") && !isKind(obj, "Service") { return summary } _, hasMatch := selector["matchLabels"] if !hasMatch { _, hasMatch = selector["matchExpressions"] } sel := metav1.LabelSelector{} if hasMatch { if err := convert.ToObj(selector, &sel); err != nil { return summary } } else { sel.MatchLabels = map[string]string{} for k, v := range selector { sel.MatchLabels[k] = convert.ToString(v) } } t := "creates" if obj["kind"] == "Service" { t = "selects" } summary.Relationships = append(summary.Relationships, Relationship{ Kind: "Pod", APIVersion: "v1", Type: t, Selector: &sel, }) return summary } func checkPod(obj data.Object, condition []Condition, summary Summary) Summary { if !isKind(obj, "Pod") { return summary } if obj.String("kind") != "Pod" || obj.String("apiVersion") != "v1" { return summary } return checkPodTemplate(obj, condition, summary) } func checkPodTemplate(obj data.Object, condition []Condition, summary Summary) Summary { summary = checkPodConfigMaps(obj, condition, summary) summary = checkPodSecrets(obj, condition, summary) summary = checkPodServiceAccount(obj, condition, summary) summary = checkPodProjectedVolume(obj, condition, summary) summary = checkPodPullSecret(obj, condition, summary) return summary } func checkPodPullSecret(obj data.Object, _ []Condition, summary Summary) Summary { for _, pullSecret := range obj.Slice("imagePullSecrets") { if name := pullSecret.String("name"); name != "" { summary.Relationships = append(summary.Relationships, Relationship{ Name: name, Kind: "Secret", APIVersion: "v1", Type: "uses", }) } } return summary } func checkPodProjectedVolume(obj data.Object, _ []Condition, summary Summary) Summary { for _, vol := range obj.Slice("spec", "volumes") { for _, source := range vol.Slice("projected", "sources") { if secretName := source.String("secret", "name"); secretName != "" { summary.Relationships = append(summary.Relationships, Relationship{ Name: secretName, Kind: "Secret", APIVersion: "v1", Type: "uses", }) } if configMap := source.String("configMap", "name"); configMap != "" { summary.Relationships = append(summary.Relationships, Relationship{ Name: configMap, Kind: "ConfigMap", APIVersion: "v1", Type: "uses", }) } } } return summary } func addEnvRef(summary Summary, names map[string]bool, obj data.Object, fieldPrefix, kind string) Summary { for _, container := range obj.Slice("spec", "containers") { for _, env := range container.Slice("envFrom") { name := env.String(fieldPrefix+"Ref", "name") if name == "" || names[name] { continue } names[name] = true summary.Relationships = append(summary.Relationships, Relationship{ Name: name, Kind: kind, APIVersion: "v1", Type: "uses", }) } for _, env := range container.Slice("env") { name := env.String("valueFrom", fieldPrefix+"KeyRef", "name") if name == "" || names[name] { continue } names[name] = true summary.Relationships = append(summary.Relationships, Relationship{ Name: name, Kind: kind, APIVersion: "v1", Type: "uses", }) } } return summary } func checkPodConfigMaps(obj data.Object, _ []Condition, summary Summary) Summary { names := map[string]bool{} for _, vol := range obj.Slice("spec", "volumes") { name := vol.String("configMap", "name") if name == "" || names[name] { continue } names[name] = true summary.Relationships = append(summary.Relationships, Relationship{ Name: name, Kind: "ConfigMap", APIVersion: "v1", Type: "uses", }) } summary = addEnvRef(summary, names, obj, "configMap", "ConfigMap") return summary } func checkPodSecrets(obj data.Object, _ []Condition, summary Summary) Summary { names := map[string]bool{} for _, vol := range obj.Slice("spec", "volumes") { name := vol.String("secret", "secretName") if name == "" || names[name] { continue } names[name] = true summary.Relationships = append(summary.Relationships, Relationship{ Name: name, Kind: "Secret", APIVersion: "v1", Type: "uses", }) } summary = addEnvRef(summary, names, obj, "secret", "Secret") return summary } func checkPodServiceAccount(obj data.Object, _ []Condition, summary Summary) Summary { saName := obj.String("spec", "serviceAccountName") summary.Relationships = append(summary.Relationships, Relationship{ Name: saName, Kind: "ServiceAccount", APIVersion: "v1", Type: "uses", }) return summary } ================================================ FILE: pkg/summary/gvk.go ================================================ package summary import ( "encoding/json" "fmt" "regexp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" ) var ( gvkRegExp = regexp.MustCompile(`^(.*?)/(.*),\s*Kind=(.+)$`) ) // conditionTypeStatusJSON is a custom JSON to map a complex object into a standard JSON object. It maps Groups, // Versions and Kinds to Conditions, Types, and Status, indicating with a flag if a certain condition with specific // status represents an error or not. It is expected to be something like: // // { // "gvk": "helm.cattle.io/v1, Kind=HelmChart", // "conditionMappings": [ // { // "type": "JobCreated" // This means JobCreated is mostly informational and True or False // }, // doesn't mean error // { // "type": "Failed", // This means Failed is considered error if it's status is True // "status": ["True"] // }, // } // } type conditionTypeStatusJSON struct { GVK string `json:"gvk"` ConditionMappings []conditionStatusErrorJSON `json:"conditionMappings"` } type conditionStatusErrorJSON struct { Type string `json:"type"` Status []metav1.ConditionStatus `json:"status,omitempty"` } type ConditionTypeStatusErrorMapping map[schema.GroupVersionKind]map[string]sets.Set[metav1.ConditionStatus] func (m ConditionTypeStatusErrorMapping) MarshalJSON() ([]byte, error) { output := []conditionTypeStatusJSON{} for gvk, mapping := range m { typeStatus := conditionTypeStatusJSON{GVK: gvk.String()} for condition, statuses := range mapping { typeStatus.ConditionMappings = append(typeStatus.ConditionMappings, conditionStatusErrorJSON{ Type: condition, Status: statuses.UnsortedList(), }) } output = append(output, typeStatus) } return json.Marshal(output) } func (m ConditionTypeStatusErrorMapping) UnmarshalJSON(data []byte) error { var conditionMappingsJSON []conditionTypeStatusJSON err := json.Unmarshal(data, &conditionMappingsJSON) if err != nil { return err } for _, mapping := range conditionMappingsJSON { // checking if mapping.GVK is in the right format: /[version], Kind=[kind] mx := gvkRegExp.FindStringSubmatch(mapping.GVK) if len(mx) == 0 { return fmt.Errorf("gvk parsing failed: wrong GVK format: <%s>", mapping.GVK) } conditionMappings := map[string]sets.Set[metav1.ConditionStatus]{} for _, condition := range mapping.ConditionMappings { conditionMappings[condition.Type] = sets.New[metav1.ConditionStatus](condition.Status...) } m[schema.GroupVersionKind{ Group: mx[1], Version: mx[2], Kind: mx[3], }] = conditionMappings } return nil } ================================================ FILE: pkg/summary/gvk_test.go ================================================ package summary import ( "encoding/json" "sort" "testing" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" ) func TestConditionalTypeStatusErrorMapping_MarshalJSON(t *testing.T) { testCases := []struct { name string input ConditionTypeStatusErrorMapping expected []byte expectedErr error }{ { name: "usual case", input: ConditionTypeStatusErrorMapping{ {Group: "helm.cattle.io", Version: "v1", Kind: "HelmChart"}: { "JobCreated": sets.New[metav1.ConditionStatus](), "Failed": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), }, }, expected: []byte(` [ { "gvk": "helm.cattle.io/v1, Kind=HelmChart", "conditionMappings": [ { "type": "JobCreated" }, { "type": "Failed", "status": ["True"] } ] } ] `), expectedErr: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { output, err := json.Marshal(&tc.input) if tc.expectedErr == nil { assert.NoError(t, err) } else { assert.Error(t, err) } actual := []conditionTypeStatusJSON{} expected := []conditionTypeStatusJSON{} assert.NoError(t, json.Unmarshal(output, &actual)) assert.NoError(t, json.Unmarshal(tc.expected, &expected)) sort.Slice(actual, func(i, j int) bool { return actual[i].GVK < actual[j].GVK }) sort.Slice(expected, func(i, j int) bool { return expected[i].GVK < expected[j].GVK }) for _, act := range actual { sort.Slice(act.ConditionMappings, func(i, j int) bool { return act.ConditionMappings[i].Type < act.ConditionMappings[j].Type }) for _, mappings := range act.ConditionMappings { sort.Slice(mappings.Status, func(i, j int) bool { return mappings.Status[i] < mappings.Status[j] }) } } for _, exp := range expected { sort.Slice(exp.ConditionMappings, func(i, j int) bool { return exp.ConditionMappings[i].Type < exp.ConditionMappings[j].Type }) for _, mappings := range exp.ConditionMappings { sort.Slice(mappings.Status, func(i, j int) bool { return mappings.Status[i] < mappings.Status[j] }) } } assert.Equal(t, expected, actual) }) } } func TestConditionalTypeStatusErrorMapping_UnmarshalJSON(t *testing.T) { testCases := []struct { name string input []byte expected ConditionTypeStatusErrorMapping errorIsExpected bool }{ { name: "usual case", input: []byte(` [ { "gvk": "helm.cattle.io/v1, Kind=HelmChart", "conditionMappings": [ { "type": "JobCreated" }, { "type": "Failed", "status": ["True"] } ] } ] `), expected: ConditionTypeStatusErrorMapping{ {Group: "helm.cattle.io", Version: "v1", Kind: "HelmChart"}: { "JobCreated": sets.New[metav1.ConditionStatus](), "Failed": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), }, }, errorIsExpected: false, }, { name: "core types (no group)", input: []byte(` [ { "gvk": "/v1, Kind=Node", "conditionMappings": [ { "type": "Ready", "status": ["False"] } ] } ] `), expected: ConditionTypeStatusErrorMapping{ {Group: "", Version: "v1", Kind: "Node"}: { "Ready": sets.New[metav1.ConditionStatus](metav1.ConditionFalse), }, }, errorIsExpected: false, }, { name: "wrong GVK format", input: []byte(` [ { "gvk": "wrong GVK format", "conditionMappings": [ { "type": "Ready", "status": ["False"] } ] } ] `), expected: ConditionTypeStatusErrorMapping{}, errorIsExpected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { gvkConditionMappings := ConditionTypeStatusErrorMapping{} err := json.Unmarshal(tc.input, &gvkConditionMappings) if tc.errorIsExpected { assert.Error(t, err) } else { assert.NoError(t, err) } assert.Equal(t, tc.expected, gvkConditionMappings) }) } } ================================================ FILE: pkg/summary/informer/informer.go ================================================ /* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package informer import ( "context" "sync" "time" "github.com/rancher/wrangler/v3/pkg/summary" "github.com/rancher/wrangler/v3/pkg/summary/client" "github.com/rancher/wrangler/v3/pkg/summary/lister" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/informers" "k8s.io/client-go/tools/cache" ) // NewSummarySharedInformerFactory constructs a new instance of summarySharedInformerFactory for all namespaces. func NewSummarySharedInformerFactory(client client.Interface, defaultResync time.Duration) SummarySharedInformerFactory { return NewFilteredSummarySharedInformerFactory(client, defaultResync, metav1.NamespaceAll, nil) } // NewFilteredSummarySharedInformerFactory constructs a new instance of summarySharedInformerFactory. // Listers obtained via this factory will be subject to the same filters as specified here. func NewFilteredSummarySharedInformerFactory(client client.Interface, defaultResync time.Duration, namespace string, tweakListOptions TweakListOptionsFunc) SummarySharedInformerFactory { return &summarySharedInformerFactory{ client: client, defaultResync: defaultResync, namespace: namespace, informers: map[schema.GroupVersionResource]informers.GenericInformer{}, startedInformers: make(map[schema.GroupVersionResource]bool), tweakListOptions: tweakListOptions, } } type summarySharedInformerFactory struct { client client.Interface defaultResync time.Duration namespace string lock sync.Mutex informers map[schema.GroupVersionResource]informers.GenericInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[schema.GroupVersionResource]bool tweakListOptions TweakListOptionsFunc } var _ SummarySharedInformerFactory = &summarySharedInformerFactory{} func (f *summarySharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer { f.lock.Lock() defer f.lock.Unlock() key := gvr informer, exists := f.informers[key] if exists { return informer } informer = NewFilteredSummaryInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) f.informers[key] = informer return informer } // Start initializes all requested informers. func (f *summarySharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Informer().Run(stopCh) f.startedInformers[informerType] = true } } } // WaitForCacheSync waits for all started informers' cache were synced. func (f *summarySharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool { informers := func() map[schema.GroupVersionResource]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informers := map[schema.GroupVersionResource]cache.SharedIndexInformer{} for informerType, informer := range f.informers { if f.startedInformers[informerType] { informers[informerType] = informer.Informer() } } return informers }() res := map[schema.GroupVersionResource]bool{} for informType, informer := range informers { res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) } return res } func NewFilteredSummaryInformerWithOptions( client client.ExtendedInterface, gvr schema.GroupVersionResource, opts *client.Options, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc, ) informers.GenericInformer { lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.ResourceWithOptions(gvr, opts).Namespace(namespace).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.ResourceWithOptions(gvr, opts).Namespace(namespace).Watch(context.TODO(), options) }, } return &summaryInformer{ gvr: gvr, informer: cache.NewSharedIndexInformer( toListWatcherWithWatchListSemantics(lw, client), &summary.SummarizedObject{}, resyncPeriod, indexers, ), } } // NewFilteredSummaryInformer constructs a new informer for a summary type. func NewFilteredSummaryInformer(client client.Interface, gvr schema.GroupVersionResource, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc) informers.GenericInformer { lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.Resource(gvr).Namespace(namespace).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.Resource(gvr).Namespace(namespace).Watch(context.TODO(), options) }, } return &summaryInformer{ gvr: gvr, informer: cache.NewSharedIndexInformer( toListWatcherWithWatchListSemantics(lw, client), &summary.SummarizedObject{}, resyncPeriod, indexers, ), } } type summaryInformer struct { informer cache.SharedIndexInformer gvr schema.GroupVersionResource } var _ informers.GenericInformer = &summaryInformer{} func (d *summaryInformer) Informer() cache.SharedIndexInformer { return d.informer } func (d *summaryInformer) Lister() cache.GenericLister { return lister.NewRuntimeObjectShim(lister.New(d.informer.GetIndexer(), d.gvr)) } ================================================ FILE: pkg/summary/informer/informer_test.go ================================================ package informer import ( "context" "testing" "time" "github.com/rancher/wrangler/v3/pkg/summary" "github.com/rancher/wrangler/v3/pkg/summary/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/cache" ) var _ client.ExtendedInterface = (*mockClient)(nil) type mockClient struct { listCalled chan struct{} watchCalled chan struct{} } func (m *mockClient) Resource(resource schema.GroupVersionResource) client.NamespaceableResourceInterface { return m } func (m *mockClient) ResourceWithOptions(resource schema.GroupVersionResource, opts *client.Options) client.NamespaceableResourceInterface { return m } func (m *mockClient) Namespace(string) client.ResourceInterface { return m } func (m *mockClient) List(ctx context.Context, opts metav1.ListOptions) (*summary.SummarizedObjectList, error) { if m.listCalled != nil { m.listCalled <- struct{}{} } return &summary.SummarizedObjectList{ ListMeta: metav1.ListMeta{ ResourceVersion: "1", }, }, nil } func (m *mockClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { if m.watchCalled != nil { m.watchCalled <- struct{}{} } return watch.NewFake(), nil } type mockClientUnsupported struct { mockClient } func (m *mockClientUnsupported) IsWatchListSemanticsUnSupported() bool { return true } type mockClientSupported struct { mockClient } func (m *mockClientSupported) IsWatchListSemanticsUnSupported() bool { return false } func TestNewFilteredSummaryInformer_WatchListSupport(t *testing.T) { gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"} namespace := "default" tests := []struct { name string client client.Interface expectList bool expectWatch bool }{ { name: "Client supporting watchlist (default)", client: &mockClient{ listCalled: make(chan struct{}, 1), watchCalled: make(chan struct{}, 1), }, expectList: false, }, { name: "Client explicitly supporting watchlist", client: &mockClientSupported{ mockClient: mockClient{ listCalled: make(chan struct{}, 1), watchCalled: make(chan struct{}, 1), }, }, expectList: false, }, { name: "Client explicitly NOT supporting watchlist", client: &mockClientUnsupported{ mockClient: mockClient{ listCalled: make(chan struct{}, 1), watchCalled: make(chan struct{}, 1), }, }, expectList: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { informer := NewFilteredSummaryInformer(tt.client, gvr, namespace, 0, cache.Indexers{}, nil) stopCh := make(chan struct{}) defer close(stopCh) go informer.Informer().Run(stopCh) // Wait for either List or Watch to be called var mc *mockClient switch c := tt.client.(type) { case *mockClient: mc = c case *mockClientSupported: mc = &c.mockClient case *mockClientUnsupported: mc = &c.mockClient } time.Sleep(100 * time.Millisecond) listCalled := false watchCalled := false select { case <-time.After(100 * time.Millisecond): case <-mc.listCalled: listCalled = true } select { case <-time.After(100 * time.Millisecond): case <-mc.watchCalled: watchCalled = true } if tt.expectList && !listCalled { t.Fatal("Expected list call but didn't get it") } if !tt.expectList && listCalled { t.Fatal("Expected NO list call") } if !watchCalled { t.Fatal("Expected watch call but didn't get it") } }) } } func TestNewFilteredSummaryInformerWithOptions_WatchListSupport(t *testing.T) { gvr := schema.GroupVersionResource{Group: "test", Version: "v1", Resource: "tests"} namespace := "default" tests := []struct { name string client client.ExtendedInterface expectList bool expectWatch bool }{ { name: "Client supporting watchlist (default)", client: &mockClient{ listCalled: make(chan struct{}, 1), watchCalled: make(chan struct{}, 1), }, expectList: false, }, { name: "Client explicitly supporting watchlist", client: &mockClientSupported{ mockClient: mockClient{ listCalled: make(chan struct{}, 1), watchCalled: make(chan struct{}, 1), }, }, expectList: false, }, { name: "Client explicitly NOT supporting watchlist", client: &mockClientUnsupported{ mockClient: mockClient{ listCalled: make(chan struct{}, 1), watchCalled: make(chan struct{}, 1), }, }, expectList: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { informer := NewFilteredSummaryInformerWithOptions(tt.client, gvr, nil, namespace, 0, cache.Indexers{}, nil) stopCh := make(chan struct{}) defer close(stopCh) go informer.Informer().Run(stopCh) // Wait for either List or Watch to be called var mc *mockClient switch c := tt.client.(type) { case *mockClient: mc = c case *mockClientSupported: mc = &c.mockClient case *mockClientUnsupported: mc = &c.mockClient } time.Sleep(100 * time.Millisecond) listCalled := false watchCalled := false select { case <-time.After(100 * time.Millisecond): case <-mc.listCalled: listCalled = true } select { case <-time.After(100 * time.Millisecond): case <-mc.watchCalled: watchCalled = true } if tt.expectList && !listCalled { t.Fatal("Expected list call but didn't get it") } if !tt.expectList && listCalled { t.Fatal("Expected NO list call") } if !watchCalled { t.Fatal("Expected watch call but didn't get it") } }) } } ================================================ FILE: pkg/summary/informer/interface.go ================================================ /* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package informer import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/informers" ) // SummarySharedInformerFactory provides access to a shared informer and lister for dynamic client type SummarySharedInformerFactory interface { Start(stopCh <-chan struct{}) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool } // TweakListOptionsFunc defines the signature of a helper function // that wants to provide more listing options to API type TweakListOptionsFunc func(*metav1.ListOptions) ================================================ FILE: pkg/summary/informer/watchlist.go ================================================ package informer import ( "k8s.io/client-go/tools/cache" ) // Copied from Kubernetes' source, only available in k8s 1.35 type listWatcherWithWatchListSemanticsWrapper struct { *cache.ListWatch // unsupportedWatchListSemantics indicates whether a client explicitly does NOT support // WatchList semantics. // // Over the years, unit tests in kube have been written in many different ways. // After enabling the WatchListClient feature by default, existing tests started failing. // To avoid breaking lots of existing client-go users after upgrade, // we introduced this field as an opt-in. // // When true, the reflector disables WatchList even if the feature gate is enabled. unsupportedWatchListSemantics bool } func (lw *listWatcherWithWatchListSemanticsWrapper) IsWatchListSemanticsUnSupported() bool { return lw.unsupportedWatchListSemantics } // toListWatcherWithWatchListSemantics returns a ListerWatcher // that knows whether the provided client explicitly // does NOT support the WatchList semantics. This allows Reflectors // to adapt their behavior based on client capabilities. func toListWatcherWithWatchListSemantics(lw *cache.ListWatch, client any) cache.ListerWatcher { return &listWatcherWithWatchListSemanticsWrapper{ lw, doesClientNotSupportWatchListSemantics(client), } } type unSupportedWatchListSemantics interface { IsWatchListSemanticsUnSupported() bool } // doesClientNotSupportWatchListSemantics reports whether the given client // does NOT support WatchList semantics. // // A client does NOT support WatchList only if // it implements `IsWatchListSemanticsUnSupported` and that returns true. func doesClientNotSupportWatchListSemantics(client any) bool { lw, ok := client.(unSupportedWatchListSemantics) if !ok { return false } return lw.IsWatchListSemanticsUnSupported() } ================================================ FILE: pkg/summary/lister/interface.go ================================================ /* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package lister import ( "github.com/rancher/wrangler/v3/pkg/summary" "k8s.io/apimachinery/pkg/labels" ) // Lister helps list resources. type Lister interface { // List lists all resources in the indexer. List(selector labels.Selector) (ret []*summary.SummarizedObject, err error) // Get retrieves a resource from the indexer with the given name Get(name string) (*summary.SummarizedObject, error) // Namespace returns an object that can list and get resources in a given namespace. Namespace(namespace string) NamespaceLister } // NamespaceLister helps list and get resources. type NamespaceLister interface { // List lists all resources in the indexer for a given namespace. List(selector labels.Selector) (ret []*summary.SummarizedObject, err error) // Get retrieves a resource from the indexer for a given namespace and name. Get(name string) (*summary.SummarizedObject, error) } ================================================ FILE: pkg/summary/lister/lister.go ================================================ /* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package lister import ( "github.com/rancher/wrangler/v3/pkg/summary" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/cache" ) var _ Lister = &summaryLister{} var _ NamespaceLister = &summaryNamespaceLister{} // summaryLister implements the Lister interface. type summaryLister struct { indexer cache.Indexer gvr schema.GroupVersionResource } // New returns a new Lister. func New(indexer cache.Indexer, gvr schema.GroupVersionResource) Lister { return &summaryLister{indexer: indexer, gvr: gvr} } // List lists all resources in the indexer. func (l *summaryLister) List(selector labels.Selector) (ret []*summary.SummarizedObject, err error) { err = cache.ListAll(l.indexer, selector, func(m interface{}) { ret = append(ret, m.(*summary.SummarizedObject)) }) return ret, err } // Get retrieves a resource from the indexer with the given name func (l *summaryLister) Get(name string) (*summary.SummarizedObject, error) { obj, exists, err := l.indexer.GetByKey(name) if err != nil { return nil, err } if !exists { return nil, errors.NewNotFound(l.gvr.GroupResource(), name) } return obj.(*summary.SummarizedObject), nil } // Namespace returns an object that can list and get resources from a given namespace. func (l *summaryLister) Namespace(namespace string) NamespaceLister { return &summaryNamespaceLister{indexer: l.indexer, namespace: namespace, gvr: l.gvr} } // summaryNamespaceLister implements the NamespaceLister interface. type summaryNamespaceLister struct { indexer cache.Indexer namespace string gvr schema.GroupVersionResource } // List lists all resources in the indexer for a given namespace. func (l *summaryNamespaceLister) List(selector labels.Selector) (ret []*summary.SummarizedObject, err error) { err = cache.ListAllByNamespace(l.indexer, l.namespace, selector, func(m interface{}) { ret = append(ret, m.(*summary.SummarizedObject)) }) return ret, err } // Get retrieves a resource from the indexer for a given namespace and name. func (l *summaryNamespaceLister) Get(name string) (*summary.SummarizedObject, error) { obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name) if err != nil { return nil, err } if !exists { return nil, errors.NewNotFound(l.gvr.GroupResource(), name) } return obj.(*summary.SummarizedObject), nil } ================================================ FILE: pkg/summary/lister/shim.go ================================================ /* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package lister import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" ) var _ cache.GenericLister = &summaryListerShim{} var _ cache.GenericNamespaceLister = &summaryNamespaceListerShim{} // summaryListerShim implements the cache.GenericLister interface. type summaryListerShim struct { lister Lister } // NewRuntimeObjectShim returns a new shim for Lister. // It wraps Lister so that it implements cache.GenericLister interface func NewRuntimeObjectShim(lister Lister) cache.GenericLister { return &summaryListerShim{lister: lister} } // List will return all objects across namespaces func (s *summaryListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { objs, err := s.lister.List(selector) if err != nil { return nil, err } ret = make([]runtime.Object, len(objs)) for index, obj := range objs { ret[index] = obj } return ret, err } // Get will attempt to retrieve assuming that name==key func (s *summaryListerShim) Get(name string) (runtime.Object, error) { return s.lister.Get(name) } func (s *summaryListerShim) ByNamespace(namespace string) cache.GenericNamespaceLister { return &summaryNamespaceListerShim{ namespaceLister: s.lister.Namespace(namespace), } } // summaryNamespaceListerShim implements the NamespaceLister interface. // It wraps NamespaceLister so that it implements cache.GenericNamespaceLister interface type summaryNamespaceListerShim struct { namespaceLister NamespaceLister } // List will return all objects in this namespace func (ns *summaryNamespaceListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { objs, err := ns.namespaceLister.List(selector) if err != nil { return nil, err } ret = make([]runtime.Object, len(objs)) for index, obj := range objs { ret[index] = obj } return ret, err } // Get will attempt to retrieve by namespace and name func (ns *summaryNamespaceListerShim) Get(name string) (runtime.Object, error) { return ns.namespaceLister.Get(name) } ================================================ FILE: pkg/summary/summarized.go ================================================ package summary import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) type SummarizedObject struct { metav1.PartialObjectMetadata Summary } type SummarizedObjectList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` Items []SummarizedObject `json:"items" protobuf:"bytes,2,rep,name=items"` } func Summarized(u runtime.Object) *SummarizedObject { return SummarizedWithOptions(u, nil) } func SummarizedWithOptions(u runtime.Object, opts *SummarizeOptions) *SummarizedObject { if s, ok := u.(*SummarizedObject); ok { return s } s := &SummarizedObject{ Summary: SummarizeWithOptions(u, opts), } s.APIVersion, s.Kind = u.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() meta, err := meta.Accessor(u) if err == nil { s.Name = meta.GetName() s.Namespace = meta.GetNamespace() s.Generation = meta.GetGeneration() s.UID = meta.GetUID() s.ResourceVersion = meta.GetResourceVersion() s.CreationTimestamp = meta.GetCreationTimestamp() s.DeletionTimestamp = meta.GetDeletionTimestamp() s.Labels = meta.GetLabels() s.Annotations = meta.GetAnnotations() } return s } func (in *SummarizedObjectList) DeepCopyInto(out *SummarizedObjectList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]SummarizedObject, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } func (in *SummarizedObjectList) DeepCopy() *SummarizedObjectList { if in == nil { return nil } out := new(SummarizedObjectList) in.DeepCopyInto(out) return out } func (in *SummarizedObjectList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } func (in *SummarizedObject) DeepCopyInto(out *SummarizedObject) { *out = *in out.TypeMeta = in.TypeMeta out.ObjectMeta = *in.ObjectMeta.DeepCopy() out.Summary = *in.Summary.DeepCopy() } func (in *SummarizedObject) DeepCopy() *SummarizedObject { if in == nil { return nil } out := new(SummarizedObject) in.DeepCopyInto(out) return out } func (in *SummarizedObject) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } ================================================ FILE: pkg/summary/summarizers.go ================================================ package summary import ( "encoding/json" "fmt" "os" "strings" "time" "github.com/rancher/wrangler/v3/pkg/data" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/kv" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" ) const ( kindSep = ", Kind=" reason = "%REASON%" checkGVKErrorMappingEnvVar = "CATTLE_WRANGLER_CHECK_GVK_ERROR_MAPPING" ) var ( // True == // False == error // Unknown == transitioning TransitioningUnknown = map[string]string{ "Active": "activating", "AddonDeploy": "provisioning", "AgentDeployed": "provisioning", "BackingNamespaceCreated": "configuring", "Built": "building", "CertsGenerated": "provisioning", "ConfigOK": "configuring", "Created": "creating", "CreatorMadeOwner": "configuring", "DefaultNamespaceAssigned": "configuring", "DefaultNetworkPolicyCreated": "configuring", "DefaultProjectCreated": "configuring", "DockerProvisioned": "provisioning", "Deployed": "deploying", "Drained": "draining", "Downloaded": "downloading", "etcd": "provisioning", "Inactive": "deactivating", "Initialized": "initializing", "Installed": "installing", "NodesCreated": "provisioning", "Pending": "pending", "PodScheduled": "scheduling", "Provisioned": "provisioning", "Reconciled": "reconciling", // CAPI Machine, RKEControlPlane "Refreshed": "refreshed", "Registered": "registering", "Removed": "removing", "Saved": "saving", "Updated": "updating", "Updating": "updating", "Upgraded": "upgrading", "Waiting": "waiting", "InitialRolesPopulated": "activating", "ScalingActive": "pending", "AbleToScale": "pending", "RunCompleted": "running", "Processed": "processed", "NodeHealthy": reason, // CAPI Machine "NodeReady": reason, // CAPI Machine } // For given GVK, This condition Type and this Status, indicates an error or not // e.g.: GVK: helm.cattle.io/v1, HelmChart // --> JobCreated: [], indicates True or False are not errors // --> Failed: ["True"], indicates "True" status is considered error // --> Worked: ["False"], indicates "False" status is considered error // --> Unknown: ["True", "False"] indicated "True" or "False" are considered errors GVKConditionErrorMapping = ConditionTypeStatusErrorMapping{ {Group: "helm.cattle.io", Version: "v1", Kind: "HelmChart"}: { "JobCreated": sets.New[metav1.ConditionStatus](), "Failed": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), }, {Group: "", Version: "v1", Kind: "Node"}: { "OutOfDisk": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), "MemoryPressure": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), "DiskPressure": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), "NetworkUnavailable": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), }, {Group: "apps", Version: "v1", Kind: "Deployment"}: { "ReplicaFailure": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), "Progressing": sets.New[metav1.ConditionStatus](metav1.ConditionFalse), }, {Group: "apps", Version: "v1", Kind: "ReplicaSet"}: { "ReplicaFailure": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), }, // FALLBACK: In case we cannot match any Groups, Versions and Kinds then we fallback to this mapping. {Group: "", Version: "", Kind: ""}: { "Stalled": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), "Failed": sets.New[metav1.ConditionStatus](metav1.ConditionTrue), }, } // True == // False == transitioning // Unknown == error TransitioningFalse = map[string]string{ "Completed": "activating", "Ready": "unavailable", "Available": "updating", "BootstrapConfigReady": reason, // CAPI Machine "InfrastructureReady": reason, // CAPI Machine "MachinesReady": "updating", // CAPI MachineDeployment, MachineSet } // True == transitioning // False == // Unknown == error TransitioningTrue = map[string]string{ "Reconciling": "reconciling", "ScalingUp": reason, // CAPI Cluster, MachineDeployment, MachineSet "ScalingDown": reason, // CAPI Cluster, MachineDeployment, MachineSet "Deleting": reason, // CAPI Cluster, MachineDeployment, MachineSet, Machine "Paused": reason, // CAPI Cluster, MachineDeployment, MachineSet, Machine } Summarizers []Summarizer ConditionSummarizers []Summarizer ) type Summarizer func(obj data.Object, conditions []Condition, summary Summary) Summary func init() { ConditionSummarizers = []Summarizer{ checkErrors, checkGenericTransitioning, checkRemoving, checkCattleReady, } Summarizers = []Summarizer{ checkStatusSummary, checkErrors, checkTransitioning, checkActive, checkPhase, checkInitializing, checkRemoving, checkStandard, checkLoadBalancer, checkPod, checkHasPodSelector, checkHasPodTemplate, checkOwner, checkApplyOwned, checkCattleTypes, checkGeneration, } initializeCheckErrors() } func initializeCheckErrors() { gvkConfig := os.Getenv(checkGVKErrorMappingEnvVar) if gvkConfig != "" { logrus.Debugf("GVK Error Mapping Provided") gvkErrorMapping := ConditionTypeStatusErrorMapping{} if err := json.Unmarshal([]byte(gvkConfig), &gvkErrorMapping); err != nil { logrus.Errorln("Unable to parse GVK config: ", err.Error()) return } // Merging GVK + Conditions // // IMPORTANT: In case you add a condition that exists already, we replace the set that holds the Status // completely of that condition by yours, this makes it possible to deactivate certain statuses for // debugging reasons. // // eg.: // // Existing one: // // helm.cattle.io, Kind=HelmChart // JobCreated => [] // Failed => ["True"] // // In case you set Failed = ["False"] and add Ready = ["False"]: // // helm.cattle.io, Kind=HelmChart // JobCreated => [] <<<= not changed // Failed => ["False"] <<<= replaced completely the set. // Ready => ["False"] <<<= merged to existing conditions. // // So, we've merged the conditions, but not the status set values. for gvk, newConditionsMap := range gvkErrorMapping { if _, exists := GVKConditionErrorMapping[gvk]; !exists { GVKConditionErrorMapping[gvk] = map[string]sets.Set[metav1.ConditionStatus]{} } existingConditionsMap := GVKConditionErrorMapping[gvk] for condition, errorMapping := range newConditionsMap { existingConditionsMap[condition] = errorMapping } GVKConditionErrorMapping[gvk] = existingConditionsMap } logrus.Debugf("GVK Error Mapping Set") return } logrus.Debugf("GVK Error Mapping not provided, using predefined values") } func checkGeneration(obj data.Object, _ []Condition, summary Summary) Summary { if summary.State != "" { return summary } if summary.HasObservedGeneration { metadataGeneration, metadataFound, errMetadata := unstructured.NestedInt64(obj, "metadata", "generation") if errMetadata != nil { return summary } if !metadataFound { return summary } observedGeneration, _, errObserved := unstructured.NestedInt64(obj, "status", "observedGeneration") if errObserved != nil { return summary } if observedGeneration != metadataGeneration { summary.State = "in-progress" summary.Transitioning = true } } return summary } func checkOwner(obj data.Object, conditions []Condition, summary Summary) Summary { ustr := &unstructured.Unstructured{ Object: obj, } for _, ownerref := range ustr.GetOwnerReferences() { rel := Relationship{ Name: ownerref.Name, Kind: ownerref.Kind, APIVersion: ownerref.APIVersion, Type: "owner", Inbound: true, } if ownerref.Controller != nil && *ownerref.Controller { rel.ControlledBy = true } summary.Relationships = append(summary.Relationships, rel) } return summary } func checkStatusSummary(obj data.Object, _ []Condition, summary Summary) Summary { summaryObj := obj.Map("status", "display") if len(summaryObj) == 0 { summaryObj = obj.Map("status", "summary") if len(summaryObj) == 0 { return summary } } obj = summaryObj if _, ok := obj["state"]; ok { summary.State = obj.String("state") } if _, ok := obj["transitioning"]; ok { summary.Transitioning = obj.Bool("transitioning") } if _, ok := obj["error"]; ok { summary.Error = obj.Bool("error") } if _, ok := obj["message"]; ok { summary.Message = append(summary.Message, obj.String("message")) } return summary } func checkStandard(obj data.Object, _ []Condition, summary Summary) Summary { if summary.State != "" { return summary } // this is a hack to not call the standard summarizers on norman mapped objects if strings.HasPrefix(obj.String("type"), "/") { return summary } result, err := kstatus.Compute(&unstructured.Unstructured{Object: obj}) if err != nil { return summary } switch result.Status { case kstatus.InProgressStatus: summary.State = "in-progress" summary.Message = append(summary.Message, result.Message) summary.Transitioning = true case kstatus.FailedStatus: summary.State = "failed" summary.Message = append(summary.Message, result.Message) summary.Error = true case kstatus.CurrentStatus: summary.State = "active" summary.Message = append(summary.Message, result.Message) case kstatus.TerminatingStatus: summary.State = "removing" summary.Message = append(summary.Message, result.Message) summary.Transitioning = true } return summary } func checkErrors(data data.Object, conditions []Condition, summary Summary) Summary { if len(conditions) == 0 { return summary } ustr := &unstructured.Unstructured{ Object: data, } conditionMapping, found := GVKConditionErrorMapping[ustr.GroupVersionKind()] if !found { conditionMapping = GVKConditionErrorMapping[schema.GroupVersionKind{}] } for _, c := range conditions { status, found := conditionMapping[c.Type()] reasonIsError := c.Reason() == "Error" if !found && !reasonIsError { continue } if reasonIsError || status.Has(metav1.ConditionStatus(c.Status())) { summary.Error = true summary.Message = append(summary.Message, c.Message()) if summary.State == "active" || summary.State == "" { summary.State = "error" } } } return summary } func checkTransitioning(obj data.Object, conditions []Condition, summary Summary) Summary { if isCAPIMachine(obj) { return checkCAPIMachineTransitioning(conditions, summary) } if isCAPIMachineSet(obj) || isCAPIMachineDeployment(obj) { return checkCAPIMachineSetAndDeploymentTransitioning(obj, conditions, summary) } if isCAPICluster(obj) { return checkCAPIClusterTransitioning(obj, conditions, summary) } return checkGenericTransitioning(obj, conditions, summary) } // checkCAPIMachineTransitioning computes summary state for CAPI Machine objects // using the Ready (aggregate) and Reconciled conditions from v1beta2. // // The Ready condition is a composite condition whose message aggregates // sub-condition issues as bullet points (e.g., "* InfrastructureReady: "). // The Reconciled condition is a Rancher-specific condition that indicates // whether the machine's desired state has been fully applied. // // Priority order (first match wins): // 1. Deleting=True -> state="deleting", transitioning=true // 2. Paused=True -> state="paused", transitioning=true // 3. Reconciled present and NOT True -> state="reconciling", // message=Reconciled.message(if has message), transitioning=true(if status=Unknown), error=true(if status=False) // 4. Ready=False (NotReady) — parse first bullet of Ready.message: // a. First bullet starts with "* BootstrapConfigReady:" -> state="waitingforinfrastructure" // b. First bullet starts with "* InfrastructureReady:" -> state="waitingfornoderef" // c. Otherwise -> pass through (no state set) // Message is the stripped detail text from the first bullet. // 5. Ready=Unknown -> state="reconciling", message=stripped detail from first // bullet of Ready.message, transitioning=true // 6. Ready=True (or any other case) -> pass through, no state set func checkCAPIMachineTransitioning(conditions []Condition, summary Summary) Summary { var reconciled *Condition var ready *Condition for i := range conditions { c := conditions[i] typ := c.Type() // Check Deleting and Paused first — these take absolute priority. if typ == "Deleting" && c.Status() == "True" { summary.State = "deleting" summary.Transitioning = true if msg := c.Message(); msg != "" { if strings.HasPrefix(msg, "Drain not completed yet") { msg = "Draining node" } summary.Message = append(summary.Message, msg) } return summary } if typ == "Paused" && c.Status() == "True" { summary.State = "paused" summary.Transitioning = true return summary } // Collect key conditions for priority evaluation below. switch typ { case "Reconciled": reconciled = &conditions[i] case "Ready": ready = &conditions[i] } } // Priority 3: Reconciled condition (when present and not True). if reconciled != nil && reconciled.Status() != "True" { switch reconciled.Status() { case "Unknown": summary.Transitioning = true summary.State = "reconciling" case "False": summary.Error = true summary.State = "reconciling" } if msg := reconciled.Message(); msg != "" { summary.Message = append(summary.Message, msg) } return summary } // Priority 4 & 5: Ready condition. if ready != nil { switch ready.Status() { case "False": // Parse the first bullet line to determine the state. detail, prefix := parseMessage(ready.Message()) switch prefix { case "BootstrapConfigReady": summary.Transitioning = true summary.State = "waitingforinfrastructure" if strings.HasSuffix(detail, "status.initialization.dataSecretCreated is false") { detail = "" } if detail != "" { summary.Message = append(summary.Message, detail) } case "InfrastructureReady": summary.Transitioning = true summary.State = "waitingfornoderef" if strings.HasSuffix(detail, "status.initialization.provisioned is false") { detail = "" } if detail != "" { summary.Message = append(summary.Message, detail) } } // If the first bullet doesn't match known patterns, pass through. return summary case "Unknown": summary.Transitioning = true summary.State = "reconciling" detail, prefix := parseMessage(ready.Message()) if prefix == "NodeHealthy" { if strings.HasSuffix(detail, "to report spec.providerID") { detail = "" } } if detail != "" { summary.Message = append(summary.Message, detail) } return summary } // Ready=True: pass through, let downstream summarizers handle it. } return summary } // checkCAPIMachineSetAndDeploymentTransitioning computes summary state for CAPI MachineSet // and MachineDeployment objects using v1beta2 conditions and replica counts. // // The function reads spec.replicas, status.replicas, and status.readyReplicas // to detect scaling operations, even when the controller hasn't yet updated // the ScalingUp/ScalingDown conditions (stale observedGeneration). // // Priority order (first match wins): // 1. Deleting=True -> state="deleting", transitioning=true // 2. Paused=True -> state="paused", transitioning=true // 3. RollingOut=True -> state="rollingout", transitioning=true // 4. ScalingDown=True OR status.replicas > spec.replicas -> state="scalingdown", transitioning=true // 5. ScalingUp=True OR spec.replicas > status.readyReplicas -> state="scalingup", transitioning=true // 6. Otherwise -> pass through (no state set) func checkCAPIMachineSetAndDeploymentTransitioning(obj data.Object, conditions []Condition, summary Summary) Summary { // Read replica counts from the object. We use nestedInt64 instead of // unstructured.NestedInt64 because YAML/JSON decoders store numbers as // float64, not int64. specReplicas, specFound, _ := nestedInt64(obj, "spec", "replicas") statusReplicas, statusFound, _ := nestedInt64(obj, "status", "replicas") readyReplicas, readyFound, _ := nestedInt64(obj, "status", "readyReplicas") upToDateReplicas, upToDateFound, _ := nestedInt64(obj, "status", "upToDateReplicas") var scalingUp *Condition var scalingDown *Condition var rollingOut *Condition for i := range conditions { c := conditions[i] typ := c.Type() // Priority 1 & 2: Deleting and Paused take absolute priority. if typ == "Deleting" && c.Status() == "True" { summary.State = "deleting" summary.Transitioning = true if msg := c.Message(); msg != "" { summary.Message = append(summary.Message, msg) } return summary } if typ == "Paused" && c.Status() == "True" { summary.State = "paused" summary.Transitioning = true return summary } switch typ { case "ScalingUp": scalingUp = &conditions[i] case "ScalingDown": scalingDown = &conditions[i] case "RollingOut": rollingOut = &conditions[i] } } // Priority 3: Rolling out — RollingOut=True indicates a rolling upgrade // is in progress. This takes priority over ScalingDown/ScalingUp because // those are side effects of the rollout strategy (maxSurge creates extra // machines, then old ones are deleted). Only MachineDeployments have // rolling upgrades; MachineSets do not set this condition. if rollingOut != nil && rollingOut.Status() == "True" { summary.State = "rollingout" summary.Transitioning = true if statusFound && upToDateFound { notUpToDate := statusReplicas - upToDateReplicas summary.Message = append(summary.Message, fmt.Sprintf("rolling out %d not up-to-date replicas", notUpToDate)) } return summary } // Priority 4: Scaling down — detected by condition OR replica count mismatch. scalingDownByCondition := scalingDown != nil && scalingDown.Status() == "True" scalingDownByReplicas := specFound && statusFound && statusReplicas > specReplicas if scalingDownByCondition || scalingDownByReplicas { summary.State = "scalingdown" summary.Transitioning = true if specFound && statusFound { summary.Message = append(summary.Message, fmt.Sprintf("Scaling down from %d to %d replicas, waiting for machines to be deleted", statusReplicas, specReplicas)) } return summary } // Priority 5: Scaling up — detected by spec.replicas > status.readyReplicas. // This catches both the transient ScalingUp=True period and the long tail // where ScalingUp has already gone False but the new machine isn't ready yet. scalingUpByReplicas := specFound && readyFound && specReplicas > readyReplicas scalingUpByCondition := scalingUp != nil && scalingUp.Status() == "True" if scalingUpByReplicas || scalingUpByCondition { summary.State = "scalingup" summary.Transitioning = true if specFound && readyFound { summary.Message = append(summary.Message, fmt.Sprintf("Scaling up from %d to %d replicas, waiting for machines to be ready", readyReplicas, specReplicas)) } return summary } return summary } // checkCAPIClusterTransitioning computes summary state for CAPI Cluster objects // using v1beta2 conditions and worker replica counts. // // The Cluster object has conditions like Available, ScalingUp, ScalingDown, // Deleting, Paused, WorkersAvailable, WorkerMachinesReady, ControlPlaneAvailable, // ControlPlaneInitialized, etc. Worker replica counts are under status.workers.*. // // Priority order (first match wins): // 1. Deleting=True -> state="deleting", transitioning=true // 2. Paused=True -> state="paused", transitioning=true // 3. RollingOut=True -> state="rollingout", transitioning=true // 4. ScalingDown=True OR status.workers.replicas > status.workers.desiredReplicas -> state="updating", transitioning=true // 5. ScalingUp=True OR status.workers.desiredReplicas > status.workers.readyReplicas -> state="updating", transitioning=true // 6. Available=False -> state="updating", transitioning=true // 7. Available=True (or any other case) -> pass through (no state set) func checkCAPIClusterTransitioning(obj data.Object, conditions []Condition, summary Summary) Summary { // Read worker replica counts from status.workers.*. desiredReplicas, desiredFound, _ := nestedInt64(obj, "status", "workers", "desiredReplicas") workersReplicas, workersFound, _ := nestedInt64(obj, "status", "workers", "replicas") readyReplicas, readyFound, _ := nestedInt64(obj, "status", "workers", "readyReplicas") upToDateReplicas, upToDateFound, _ := nestedInt64(obj, "status", "workers", "upToDateReplicas") var scalingUp *Condition var scalingDown *Condition var available *Condition var rollingOut *Condition for i := range conditions { c := conditions[i] typ := c.Type() // Priority 1 & 2: Deleting and Paused take absolute priority. if typ == "Deleting" && c.Status() == "True" { summary.State = "deleting" summary.Transitioning = true if msg := c.Message(); msg != "" { if strings.HasPrefix(msg, "* MachineDeployments") { msg = "waiting for workers deletion" } summary.Message = append(summary.Message, msg) } return summary } if typ == "Paused" && c.Status() == "True" { summary.State = "paused" summary.Transitioning = true return summary } switch typ { case "ScalingUp": scalingUp = &conditions[i] case "ScalingDown": scalingDown = &conditions[i] case "Available": available = &conditions[i] case "RollingOut": rollingOut = &conditions[i] } } // Priority 3: Rolling out — RollingOut=True indicates one or more // MachineDeployments are performing a rolling upgrade. This takes // priority over ScalingDown/ScalingUp because those are side effects // of the rollout strategy. if rollingOut != nil && rollingOut.Status() == "True" { summary.State = "rollingout" summary.Transitioning = true if workersFound && upToDateFound { notUpToDate := workersReplicas - upToDateReplicas summary.Message = append(summary.Message, fmt.Sprintf("rolling out %d not up-to-date replicas", notUpToDate)) } return summary } // Priority 4: Scaling down — detected by condition OR replica count mismatch. scalingDownByCondition := scalingDown != nil && scalingDown.Status() == "True" scalingDownByReplicas := desiredFound && workersFound && workersReplicas > desiredReplicas if scalingDownByCondition || scalingDownByReplicas { summary.State = "updating" summary.Transitioning = true if desiredFound && workersFound { summary.Message = append(summary.Message, fmt.Sprintf("Scaling down from %d to %d machines", workersReplicas, desiredReplicas)) } return summary } // Priority 5: Scaling up — Scaling up — detected by condition OR desired > readyReplicas. scalingUpByCondition := scalingUp != nil && scalingUp.Status() == "True" scalingUpByReplicas := desiredFound && readyFound && desiredReplicas > readyReplicas if scalingUpByCondition || scalingUpByReplicas { summary.State = "updating" summary.Transitioning = true if desiredFound && readyFound { summary.Message = append(summary.Message, fmt.Sprintf("Scaling up from %d to %d machines", readyReplicas, desiredReplicas)) } return summary } // Priority 5: Available=False — the cluster is not yet available. if available != nil { switch available.Status() { case "False": summary.Transitioning = true summary.State = "updating" // Parse the first bullet line to determine the state. detail, prefix := parseMessage(available.Message()) switch prefix { case "RemoteConnectionProbe": summary.Message = append(summary.Message, "establishing connection to control plane") case "ControlPlaneAvailable": summary.Message = append(summary.Message, "Waiting for control plane to be available") case "MachineDeployment": summary.Message = append(summary.Message, "Waiting for workers to be available") default: // If the first bullet doesn't match known patterns, pass through. if detail != "" { summary.Message = append(summary.Message, detail) } } case "Unknown": summary.Error = true summary.State = "unavailable" summary.Message = append(summary.Message, available.Message()) } // Ready=True: pass through, let downstream summarizers handle it. } // Priority 6: Available=True or no conditions — pass through. return summary } func checkGenericTransitioning(_ data.Object, conditions []Condition, summary Summary) Summary { for _, c := range conditions { newState, ok := TransitioningUnknown[c.Type()] if !ok { continue } if newState == reason { newState = c.Reason() } if c.Status() == "False" { summary.Error = true summary.State = newState summary.Message = append(summary.Message, c.Message()) } else if c.Status() == "Unknown" && summary.State == "" { summary.Transitioning = true summary.State = newState summary.Message = append(summary.Message, c.Message()) } } for _, c := range conditions { if summary.State != "" { break } newState, ok := TransitioningTrue[c.Type()] if !ok { continue } if newState == reason { newState = c.Reason() } if c.Status() == "True" { summary.Transitioning = true summary.State = newState summary.Message = append(summary.Message, c.Message()) } } ready := true readyMessage := "" for _, c := range conditions { if summary.State != "" { break } if c.Type() == "Ready" && c.Status() == "False" { ready = false readyMessage = c.Message() continue } newState, ok := TransitioningFalse[c.Type()] if !ok { continue } if newState == reason { newState = c.Reason() } if c.Status() == "False" { summary.Transitioning = true summary.State = newState summary.Message = append(summary.Message, c.Message()) } else if c.Status() == "Unknown" { summary.Error = true summary.State = newState summary.Message = append(summary.Message, c.Message()) } } if summary.State == "" && !ready { summary.Transitioning = true summary.State = "unavailable" summary.Message = append(summary.Message, readyMessage) } return summary } func checkActive(obj data.Object, _ []Condition, summary Summary) Summary { if summary.State != "" { return summary } switch obj.String("spec", "active") { case "true": summary.State = "active" case "false": summary.State = "inactive" } return summary } func checkPhase(obj data.Object, _ []Condition, summary Summary) Summary { phase := obj.String("status", "phase") if phase == "Succeeded" { summary.State = "succeeded" summary.Transitioning = false } else if phase == "Bound" { summary.State = "bound" summary.Transitioning = false } else if phase != "" && summary.State == "" { summary.State = phase } return summary } func checkInitializing(obj data.Object, conditions []Condition, summary Summary) Summary { apiVersion := obj.String("apiVersion") _, hasConditions := obj.Map("status")["conditions"] if summary.State == "" && hasConditions && len(conditions) == 0 && strings.Contains(apiVersion, "cattle.io") { val := obj.String("metadata", "created") if i, err := convert.ToTimestamp(val); err == nil { if time.Unix(i/1000, 0).Add(5 * time.Second).After(time.Now()) { summary.State = "initializing" summary.Transitioning = true } } } return summary } func checkRemoving(obj data.Object, conditions []Condition, summary Summary) Summary { removed := obj.String("metadata", "removed") if removed == "" { return summary } summary.State = "removing" summary.Transitioning = true finalizers := obj.StringSlice("metadata", "finalizers") if len(finalizers) == 0 { finalizers = obj.StringSlice("spec", "finalizers") } for _, cond := range conditions { if cond.Type() == "Removed" && (cond.Status() == "Unknown" || cond.Status() == "False") && cond.Message() != "" { summary.Message = append(summary.Message, cond.Message()) } } if len(finalizers) == 0 { return summary } _, f := kv.RSplit(finalizers[0], "controller.cattle.io/") if f == "foregroundDeletion" { f = "object cleanup" } summary.Message = append(summary.Message, "waiting on "+f) if i, err := convert.ToTimestamp(removed); err == nil { if time.Unix(i/1000, 0).Add(5 * time.Minute).Before(time.Now()) { summary.Error = true } } return summary } func checkLoadBalancer(obj data.Object, _ []Condition, summary Summary) Summary { if (summary.State == "active" || summary.State == "") && obj.String("kind") == "Service" && (obj.String("spec", "serviceKind") == "LoadBalancer" || obj.String("spec", "type") == "LoadBalancer") { addresses := obj.Slice("status", "loadBalancer", "ingress") if len(addresses) == 0 { summary.State = "pending" summary.Transitioning = true summary.Message = append(summary.Message, "Load balancer is being provisioned") } } return summary } func isKind(obj data.Object, kind string, apiGroups ...string) bool { if obj.String("kind") != kind { return false } if len(apiGroups) == 0 { return obj.String("apiVersion") == "v1" } if len(apiGroups) == 0 { apiGroups = []string{""} } for _, group := range apiGroups { switch { case group == "": if obj.String("apiVersion") == "v1" { return true } case group[len(group)-1] == '/': if strings.HasPrefix(obj.String("apiVersion"), group) { return true } default: if obj.String("apiVersion") != group { return true } } } return false } func checkApplyOwned(obj data.Object, conditions []Condition, summary Summary) Summary { if len(obj.Slice("metadata", "ownerReferences")) > 0 { return summary } annotations := obj.Map("metadata", "annotations") gvkString := convert.ToString(annotations["objectset.rio.cattle.io/owner-gvk"]) i := strings.Index(gvkString, kindSep) if i <= 0 { return summary } name := convert.ToString(annotations["objectset.rio.cattle.io/owner-name"]) namespace := convert.ToString(annotations["objectset.rio.cattle.io/owner-namespace"]) apiVersion := gvkString[:i] kind := gvkString[i+len(kindSep):] rel := Relationship{ Name: name, Namespace: namespace, Kind: kind, APIVersion: apiVersion, Type: "applies", Inbound: true, } summary.Relationships = append(summary.Relationships, rel) return summary } // isCAPIMachine returns true if the object is a CAPI Machine (cluster.x-k8s.io/*/Machine). func isCAPIMachine(obj data.Object) bool { return strings.HasPrefix(obj.String("apiVersion"), "cluster.x-k8s.io/") && obj.String("kind") == "Machine" } // isCAPIMachineSet returns true if the object is a CAPI MachineSet (cluster.x-k8s.io/*/MachineSet). func isCAPIMachineSet(obj data.Object) bool { return strings.HasPrefix(obj.String("apiVersion"), "cluster.x-k8s.io/") && obj.String("kind") == "MachineSet" } // isCAPIMachineDeployment returns true if the object is a CAPI MachineDeployment (cluster.x-k8s.io/*/MachineDeployment). func isCAPIMachineDeployment(obj data.Object) bool { return strings.HasPrefix(obj.String("apiVersion"), "cluster.x-k8s.io/") && obj.String("kind") == "MachineDeployment" } // isCAPICluster returns true if the object is a CAPI Cluster (cluster.x-k8s.io/*/Cluster). func isCAPICluster(obj data.Object) bool { return strings.HasPrefix(obj.String("apiVersion"), "cluster.x-k8s.io/") && obj.String("kind") == "Cluster" } // parseMessage parses the first bullet line from a condition message. // The condition message format is: // // \* : // \* : // // or with nested sub-bullets when the top-level detail is empty: // // \* : // \* : // // Multiple lines are separated by newlines. This function returns: // - detail: the stripped detail text (without "* ConditionType: " prefix). // When the top-level bullet has no inline detail, the detail from // the first indented sub-bullet is returned instead. // - prefix: the condition type name from the top-level bullet. func parseMessage(message string) (detail, prefix string) { if message == "" { return "", "" } lines := strings.Split(message, "\n") // Parse the first line. firstLine := strings.TrimPrefix(lines[0], "* ") // Split on ": " to separate condition type from detail. colonIdx := strings.Index(firstLine, ": ") if colonIdx < 0 { // No ": " separator. Check if the line ends with ":" (empty detail // with sub-bullets on subsequent lines). if strings.HasSuffix(firstLine, ":") { prefix = strings.TrimSuffix(firstLine, ":") } else { return firstLine, "" } } else { prefix = firstLine[:colonIdx] detail = firstLine[colonIdx+2:] } // If the top-level bullet has inline detail, return it. if detail != "" { return detail, prefix } // The top-level bullet has no inline detail (e.g., "* NodeHealthy:"). // Look for indented sub-bullets to extract the detail from. for _, line := range lines[1:] { trimmed := strings.TrimSpace(line) if !strings.HasPrefix(trimmed, "* ") { continue } // Found a sub-bullet: "* SubCondition: detail message" subContent := strings.TrimPrefix(trimmed, "* ") subColonIdx := strings.Index(subContent, ": ") if subColonIdx >= 0 { detail = subContent[subColonIdx+2:] } else { detail = subContent } break // Only use the first sub-bullet. } return detail, prefix } // nestedInt64 retrieves a nested numeric field from an unstructured object and // returns it as int64. Unlike unstructured.NestedInt64, which requires the value // to be exactly int64, this function also handles float64 (produced by JSON/YAML // decoders), json.Number, and string representations of integers. func nestedInt64(obj data.Object, fields ...string) (int64, bool, error) { val, found, err := unstructured.NestedFieldNoCopy(obj, fields...) if !found || err != nil { return 0, found, err } n, err := convert.ToNumber(val) if err != nil { return 0, false, err } return n, true, nil } ================================================ FILE: pkg/summary/summarizers_test.go ================================================ package summary import ( "os" "testing" "github.com/rancher/wrangler/v3/pkg/data" "github.com/stretchr/testify/assert" ) func TestCheckErrors(t *testing.T) { type input struct { data data.Object conditions []Condition summary Summary } type output struct { summary Summary } testCases := []struct { name string loadConditions func() input input expected output }{ { name: "gvk not detected - summary remains the same", input: input{ data: data.Object{}, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, }, { name: "gvk not found - summary remains the same", input: input{ data: data.Object{ "APIVersion": "sample.cattle.io/v1", "Kind": "Sample", }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, }, { name: "gvk found, no conditions provided", input: input{ data: data.Object{ "APIVersion": "helm.cattle.io/v1", "Kind": "HelmChart", }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, }, { name: "gvk found, condition not found", input: input{ data: data.Object{ "APIVersion": "helm.cattle.io/v1", "Kind": "HelmChart", }, conditions: []Condition{ NewCondition("JobFailed", "True", "", ""), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, }, { name: "gvk found, condition is error", input: input{ data: data.Object{ "APIVersion": "helm.cattle.io/v1", "Kind": "HelmChart", }, conditions: []Condition{ NewCondition("Failed", "True", "", "Helm Install Error"), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: true, Message: []string{ "Helm Install Error", }, }, }, }, { name: "gvk found, condition is not an error", input: input{ data: data.Object{ "APIVersion": "helm.cattle.io/v1", "Kind": "HelmChart", }, conditions: []Condition{ NewCondition("Failed", "False", "", ""), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, }, { name: "load conditions - gvk not found", input: input{ data: data.Object{ "APIVersion": "helm.cattle.io/v1", "Kind": "HelmChart", }, conditions: []Condition{ NewCondition("Failed", "False", "", ""), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, loadConditions: func() { os.Setenv(checkGVKErrorMappingEnvVar, ` [ { "gvk": "sample.cattle.io/v1, Kind=Sample", "conditionMappings": [ { "type": "Failed", "status": ["True"] } ] } ] `) }, }, { name: "load conditions - gvk found - condition is only informational", input: input{ data: data.Object{ "APIVersion": "sample.cattle.io/v1", "Kind": "Sample", }, conditions: []Condition{ NewCondition("Created", "True", "", ""), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, loadConditions: func() { os.Setenv(checkGVKErrorMappingEnvVar, ` [ { "gvk": "sample.cattle.io/v1, Kind=Sample", "conditionMappings": [ { "type": "Created", "status": [] } ] } ] `) }, }, { name: "load conditions - gvk found - is not an error", input: input{ data: data.Object{ "APIVersion": "sample.cattle.io/v1", "Kind": "Sample", }, conditions: []Condition{ NewCondition("Failed", "False", "", ""), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: false, }, }, loadConditions: func() { os.Setenv(checkGVKErrorMappingEnvVar, ` [ { "gvk": "sample.cattle.io/v1, Kind=Sample", "conditionMappings": [ { "type": "Failed", "status": ["True"] } ] } ] `) }, }, { name: "load conditions - gvk found - is error", input: input{ data: data.Object{ "APIVersion": "sample.cattle.io/v1", "Kind": "Sample", }, conditions: []Condition{ NewCondition("Failed", "True", "", "Sample Failure"), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: true, Message: []string{ "Sample Failure", }, }, }, loadConditions: func() { os.Setenv(checkGVKErrorMappingEnvVar, ` [ { "gvk": "sample.cattle.io/v1, Kind=Sample", "conditionMappings": [ { "type": "Failed", "status": ["True"] } ] } ] `) }, }, { name: "fallback conditions", input: input{ data: data.Object{ "APIVersion": "fallback.cattle.io/v1", "Kind": "Fallback", }, conditions: []Condition{ NewCondition("Failed", "True", "", "Sample Failure"), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: true, Message: []string{ "Sample Failure", }, }, }, }, { name: "condition has error at reason field", input: input{ data: data.Object{ "APIVersion": "sample.cattle.io/v1", "Kind": "Sample", }, conditions: []Condition{ NewCondition("SampleFailed", "True", "Error", "Error in Reason"), }, summary: Summary{ State: "testing", Error: false, }, }, expected: output{ summary: Summary{ State: "testing", Error: true, Message: []string{ "Error in Reason", }, }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.loadConditions != nil { tc.loadConditions() } initializeCheckErrors() summary := checkErrors(tc.input.data, tc.input.conditions, tc.input.summary) assert.Equal(t, tc.expected.summary, summary) }) } } func TestCheckGeneration(t *testing.T) { tests := []struct { name string obj data.Object summary Summary wantState string wantTrans bool }{ { name: "generation is int, observedGeneration is int, does nothing", obj: data.Object{ "metadata": map[string]interface{}{ "generation": int(7), }, "status": map[string]interface{}{ "observedGeneration": int(6), }, }, summary: Summary{HasObservedGeneration: true, State: ""}, wantState: "", wantTrans: false, }, { name: "generation is int32, observedGeneration is int32, does nothing", obj: data.Object{ "metadata": map[string]interface{}{ "generation": int32(5), }, "status": map[string]interface{}{ "observedGeneration": int32(5), }, }, summary: Summary{HasObservedGeneration: true, State: ""}, wantState: "", wantTrans: false, }, { name: "HasObservedGeneration false, does nothing", obj: data.Object{ "metadata": map[string]interface{}{ "generation": int64(2), }, "status": map[string]interface{}{ "observedGeneration": int64(2), }, }, summary: Summary{HasObservedGeneration: false, State: ""}, wantState: "", wantTrans: false, }, { name: "metadata.generation not found, does nothing", obj: data.Object{ "status": map[string]interface{}{ "observedGeneration": int64(2), }, }, summary: Summary{HasObservedGeneration: true, State: ""}, wantState: "", wantTrans: false, }, { name: "observedGeneration equals generation, does nothing", obj: data.Object{ "metadata": map[string]interface{}{ "generation": int64(2), }, "status": map[string]interface{}{ "observedGeneration": int64(2), }, }, summary: Summary{HasObservedGeneration: true, State: ""}, wantState: "", wantTrans: false, }, { name: "observedGeneration does not equal generation, sets in-progress and transitioning", obj: data.Object{ "metadata": map[string]interface{}{ "generation": int64(3), }, "status": map[string]interface{}{ "observedGeneration": int64(2), }, }, summary: Summary{HasObservedGeneration: true, State: ""}, wantState: "in-progress", wantTrans: true, }, { name: "status does not exist, should set in-progress and transitioning", obj: data.Object{ "metadata": map[string]interface{}{ "generation": int64(5), }, }, summary: Summary{HasObservedGeneration: true, State: ""}, wantState: "in-progress", wantTrans: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := checkGeneration(tt.obj, nil, tt.summary) assert.Equal(t, tt.wantState, got.State) assert.Equal(t, tt.wantTrans, got.Transitioning) }) } } ================================================ FILE: pkg/summary/summary.go ================================================ package summary import ( "strings" "github.com/rancher/wrangler/v3/pkg/data" unstructured2 "github.com/rancher/wrangler/v3/pkg/unstructured" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) type Summary struct { State string `json:"state,omitempty"` Error bool `json:"error,omitempty"` Transitioning bool `json:"transitioning,omitempty"` Message []string `json:"message,omitempty"` Attributes map[string]interface{} `json:"-"` Relationships []Relationship `json:"-"` HasObservedGeneration bool `json:"-"` } type SummarizeOptions struct { HasObservedGeneration bool } type Relationship struct { Name string Namespace string ControlledBy bool Kind string APIVersion string Inbound bool Type string Selector *metav1.LabelSelector } func (s Summary) String() string { if !s.Transitioning && !s.Error { return s.State } var msg string if s.Transitioning { msg = "[progressing" } if s.Error { if len(msg) > 0 { msg += ",error]" } else { msg = "error]" } } else { msg += "]" } if len(s.Message) > 0 { msg = msg + " " + strings.Join(s.Message, ", ") } return msg } func (s Summary) IsReady() bool { return !s.Error && !s.Transitioning } func (s *Summary) DeepCopy() *Summary { v := *s return &v } func (s *Summary) DeepCopyInto(v *Summary) { *v = *s } func dedupMessage(messages []string) []string { seen := map[string]bool{} var result []string for _, message := range messages { message = strings.TrimSpace(message) if message == "" { continue } if seen[message] { continue } seen[message] = true result = append(result, message) } return result } func Summarize(runtimeObj runtime.Object) Summary { return SummarizeWithOptions(runtimeObj, nil) } func SummarizeWithOptions(runtimeObj runtime.Object, opts *SummarizeOptions) Summary { var ( obj data.Object err error summary Summary ) if s, ok := runtimeObj.(*SummarizedObject); ok { return s.Summary } unstr, ok := runtimeObj.(*unstructured.Unstructured) if !ok { unstr, err = unstructured2.ToUnstructured(runtimeObj) if err != nil { return summary } } if unstr != nil { obj = unstr.Object } conditions := getConditions(obj) if opts != nil { summary.HasObservedGeneration = opts.HasObservedGeneration } for _, summarizer := range Summarizers { summary = summarizer(obj, conditions, summary) } if summary.State == "" { summary.State = "active" } summary.State = strings.ToLower(summary.State) summary.Message = dedupMessage(summary.Message) return summary } ================================================ FILE: pkg/ticker/ticker.go ================================================ package ticker import ( "context" "time" ) func Context(ctx context.Context, duration time.Duration) <-chan time.Time { ticker := time.NewTicker(duration) c := make(chan time.Time) go func() { for { select { case t := <-ticker.C: c <- t case <-ctx.Done(): close(c) ticker.Stop() return } } }() return c } ================================================ FILE: pkg/trigger/evalall.go ================================================ package trigger import ( "context" "fmt" "sync/atomic" "github.com/rancher/wrangler/v3/pkg/generic" "github.com/rancher/wrangler/v3/pkg/relatedresource" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) var ( counter int64 ) type AllHandler func() error type Controller interface { AddGenericHandler(ctx context.Context, name string, handler generic.Handler) GroupVersionKind() schema.GroupVersionKind Enqueue(namespace, name string) } type Trigger interface { Trigger() OnTrigger(ctx context.Context, name string, handler AllHandler) Key() relatedresource.Key } type trigger struct { key string controller Controller } func New(controller Controller) Trigger { return &trigger{ key: fmt.Sprintf("__trigger__%d__", atomic.AddInt64(&counter, 1)), controller: controller, } } func (e *trigger) Key() relatedresource.Key { return relatedresource.Key{ Namespace: "__trigger__", Name: e.key, } } func (e *trigger) Trigger() { e.controller.Enqueue("__trigger__", e.key) } func (e *trigger) OnTrigger(ctx context.Context, name string, handler AllHandler) { e.controller.AddGenericHandler(ctx, name, func(queueKey string, _ runtime.Object) (runtime.Object, error) { if queueKey == "__trigger__/"+e.key { return nil, handler() } return nil, nil }) e.Trigger() } ================================================ FILE: pkg/unstructured/unstructured.go ================================================ package unstructured import ( "github.com/rancher/wrangler/v3/pkg/data/convert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) func ToUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) { if ustr, ok := obj.(*unstructured.Unstructured); ok { return ustr, nil } data, err := convert.EncodeToMap(obj) if err != nil { return nil, err } return &unstructured.Unstructured{ Object: data, }, nil } ================================================ FILE: pkg/webhook/match.go ================================================ package webhook import ( v1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/runtime" ) // RouteMatch type matching of admission Request to Handlers. type RouteMatch struct { handler Handler kind string resource string version string subResource string dryRun *bool group string name string namespace string operation v1.Operation objType runtime.Object } func (r *RouteMatch) admit(response *Response, request *Request) error { if r.handler != nil { return r.handler.Admit(response, request) } return nil } func (r *RouteMatch) matches(req *v1.AdmissionRequest) bool { var group, version, kind, resource string if req.RequestKind != nil { group, version, kind = req.RequestKind.Group, req.RequestKind.Version, req.RequestKind.Kind } if req.RequestResource != nil { group, version, resource = req.RequestResource.Group, req.RequestResource.Version, req.RequestResource.Resource } return checkString(r.kind, kind) && checkString(r.resource, resource) && checkString(r.subResource, req.SubResource) && checkString(r.version, version) && checkString(r.group, group) && checkString(r.name, req.Name) && checkString(r.namespace, req.Namespace) && checkString(string(r.operation), string(req.Operation)) && checkBool(r.dryRun, req.DryRun) } func (r *RouteMatch) getObjType() runtime.Object { if r.objType == nil { return defObjType } return r.objType } func checkString(expected, actual string) bool { if expected == "" { return true } return expected == actual } func checkBool(expected, actual *bool) bool { if expected == nil { return true } if actual == nil { return false } return *expected == *actual } // Pretty methods // DryRun matches admission request with the matching DryRun value. func (r *RouteMatch) DryRun(dryRun bool) *RouteMatch { r.dryRun = &dryRun; return r } // Group matches admission request with the matching Group value. func (r *RouteMatch) Group(group string) *RouteMatch { r.group = group; return r } // HandleFunc sets the handler to be called for matching admission request. func (r *RouteMatch) HandleFunc(handler HandlerFunc) *RouteMatch { r.handler = handler; return r } // Handle sets the Handler to be called for matching admission request. func (r *RouteMatch) Handle(handler Handler) *RouteMatch { r.handler = handler; return r } // Kind matches admission request with the matching Kind value. func (r *RouteMatch) Kind(kind string) *RouteMatch { r.kind = kind; return r } // Name matches admission request with the matching Name value. func (r *RouteMatch) Name(name string) *RouteMatch { r.name = name; return r } // Namespace matches admission request with the matching Namespace value. func (r *RouteMatch) Namespace(namespace string) *RouteMatch { r.namespace = namespace; return r } // Operation matches admission request with the matching Operation value. func (r *RouteMatch) Operation(operation v1.Operation) *RouteMatch { r.operation = operation; return r } // Resource matches admission request with the matching Resource value. func (r *RouteMatch) Resource(resource string) *RouteMatch { r.resource = resource; return r } // SubResource matches admission request with the matching SubResource value. func (r *RouteMatch) SubResource(sr string) *RouteMatch { r.subResource = sr; return r } // Type specifies the runtime.Object to use for decoding. func (r *RouteMatch) Type(objType runtime.Object) *RouteMatch { r.objType = objType; return r } // Version matches admission request with the matching Version value. func (r *RouteMatch) Version(version string) *RouteMatch { r.version = version; return r } // Wrappers for pretty methods // DryRun matches admission request with the matching DryRun value. func (r *Router) DryRun(dryRun bool) *RouteMatch { return r.next().DryRun(dryRun) } // Group matches admission request with the matching Group value. func (r *Router) Group(group string) *RouteMatch { return r.next().Group(group) } // HandleFunc sets the handler to be called for matching admission request. func (r *Router) HandleFunc(hf HandlerFunc) *RouteMatch { return r.next().HandleFunc(hf) } // Handle sets the Handler to be called for matching admission request. func (r *Router) Handle(handler Handler) *RouteMatch { return r.next().Handle(handler) } // Kind matches admission request with the matching Kind value. func (r *Router) Kind(kind string) *RouteMatch { return r.next().Kind(kind) } // Name matches admission request with the matching Name value. func (r *Router) Name(name string) *RouteMatch { return r.next().Name(name) } // Namespace matches admission request with the matching Namespace value. func (r *Router) Namespace(namespace string) *RouteMatch { return r.next().Namespace(namespace) } // Operation matches admission request with the matching Operation value. func (r *Router) Operation(operation v1.Operation) *RouteMatch { return r.next().Operation(operation) } // Resource matches admission request with the matching Resource value. func (r *Router) Resource(resource string) *RouteMatch { return r.next().Resource(resource) } // SubResource matches admission request with the matching SubResource value. func (r *Router) SubResource(subResource string) *RouteMatch { return r.next().SubResource(subResource) } // Type specifies the runtime.Object to use for decoding. func (r *Router) Type(objType runtime.Object) *RouteMatch { return r.next().Type(objType) } // Version matches admission request with the matching Version value. func (r *Router) Version(version string) *RouteMatch { return r.next().Version(version) } ================================================ FILE: pkg/webhook/router.go ================================================ // Package webhook holds shared code related to routing for webhook admission. package webhook import ( "context" "encoding/json" "fmt" "net/http" jsonpatch "github.com/evanphx/json-patch" "github.com/sirupsen/logrus" v1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) var ( defObjType = &unstructured.Unstructured{} jsonPatchType = v1.PatchTypeJSONPatch ) // NewRouter returns a newly allocated Router. func NewRouter() *Router { return &Router{} } // Router manages request and the calling of matching handlers. type Router struct { matches []*RouteMatch } func (r *Router) sendError(rw http.ResponseWriter, review *v1.AdmissionReview, err error) { logrus.Error(err) if review == nil || review.Request == nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } review.Response.Result = &errors.NewInternalError(err).ErrStatus writeResponse(rw, review) } func writeResponse(rw http.ResponseWriter, review *v1.AdmissionReview) { rw.Header().Set("Content-Type", "application/json") err := json.NewEncoder(rw).Encode(review) if err != nil { logrus.Errorf("Failed to write response: %s", err) } } // ServeHTTP inspects the http.Request and calls the Admit function on all matching handlers. func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { review := &v1.AdmissionReview{} err := json.NewDecoder(req.Body).Decode(review) if err != nil { r.sendError(rw, review, err) return } if review.Request == nil { r.sendError(rw, review, fmt.Errorf("request is not set")) return } response := &Response{ AdmissionResponse: v1.AdmissionResponse{ UID: review.Request.UID, }, } review.Response = &response.AdmissionResponse if err := r.admit(response, review.Request, req); err != nil { r.sendError(rw, review, err) return } writeResponse(rw, review) } func (r *Router) admit(response *Response, request *v1.AdmissionRequest, req *http.Request) error { for _, m := range r.matches { if m.matches(request) { err := m.admit(response, &Request{ AdmissionRequest: *request, Context: req.Context(), ObjTemplate: m.getObjType(), }) logrus.Debugf("admit result: %s %s %s user=%s allowed=%v err=%v", request.Operation, request.Kind.String(), resourceString(request.Namespace, request.Name), request.UserInfo.Username, response.Allowed, err) return err } } return fmt.Errorf("no route match found for %s %s %s", request.Operation, request.Kind.String(), resourceString(request.Namespace, request.Name)) } func (r *Router) next() *RouteMatch { match := &RouteMatch{} r.matches = append(r.matches, match) return match } // Request wrapper for an AdmissionRequest. type Request struct { v1.AdmissionRequest Context context.Context ObjTemplate runtime.Object } // DecodeOldObject decodes the OldObject in the request into a new runtime.Object of type specified by Type(). // If Type() was not set the runtime.Object will be of type *unstructured.Unstructured. func (r *Request) DecodeOldObject() (runtime.Object, error) { obj := r.ObjTemplate.DeepCopyObject() err := json.Unmarshal(r.OldObject.Raw, obj) return obj, err } // DecodeObject decodes the Object in the request into a new runtime.Object of type specified by Type(). // If Type() was not set the runtime.Object will be of type *unstructured.Unstructured. func (r *Request) DecodeObject() (runtime.Object, error) { obj := r.ObjTemplate.DeepCopyObject() err := json.Unmarshal(r.Object.Raw, obj) return obj, err } // Response a wrapper for AdmissionResponses object type Response struct { v1.AdmissionResponse } // CreatePatch will patch the Object in the request with the given object. // An error will be returned if on subsequent calls to the same request. func (r *Response) CreatePatch(request *Request, newObj runtime.Object) error { if len(r.Patch) > 0 { return fmt.Errorf("response patch has already been already been assigned") } newBytes, err := json.Marshal(newObj) if err != nil { return err } patch, err := jsonpatch.CreateMergePatch(request.Object.Raw, newBytes) if err != nil { return err } r.Patch = patch r.PatchType = &jsonPatchType return nil } // The Handler type is an adapter to allow admission checking on a given request. // Handlers should update the response to control admission. type Handler interface { Admit(resp *Response, req *Request) error } // HandlerFunc type is used to add regular functions as Handler. type HandlerFunc func(resp *Response, req *Request) error // Admit calls the handler function so that the function conforms to the Handler interface. func (h HandlerFunc) Admit(resp *Response, req *Request) error { return h(resp, req) } // resourceString returns the resource formatted as a string. func resourceString(ns, name string) string { if ns == "" { return name } return fmt.Sprintf("%s/%s", ns, name) } ================================================ FILE: pkg/yaml/objects_test.go ================================================ package yaml import ( "encoding/json" "fmt" "strings" ) // JSONstruct is a test struct for verifying Unmarshal. type JSONstruct struct { EmbeddedStruct CustomField CustomStruct MismatchField bool `json:"newFieldName"` NestedField EmbeddedStruct NormalField int } type EmbeddedStruct struct { EmbeddedField string } type CustomStruct struct { Name string Namespace string } func (c *CustomStruct) UnmarshalJSON(data []byte) error { var tmp string if err := json.Unmarshal(data, &tmp); err != nil { return err } parts := strings.Split(tmp, "/") if len(parts) != 2 { return fmt.Errorf("invalid test string") } c.Name = parts[0] c.Namespace = parts[1] return nil } var ( emptyDoc = []byte("") singleString = []byte("string") unknownDoc = []byte("unknown: value") invalidYAML = []byte(`umm: 21:`) singleDeployment = []byte(`apiVersion: apps/v1 kind: Deployment metadata: name: singleDeployment spec: replicas: 3 `) multipleDeployments = []byte(`apiVersion: apps/v1 kind: Deployment metadata: name: dep1 spec: replicas: 3 --- apiVersion: apps/v1 kind: Deployment metadata: name: dep2 namespace: dep2-ns spec: paused: true --- metadata: name: dep3 namespace: dep3-ns labels: app: testapp status: readyReplicas: 4 `) jsonYAML = []byte(`normalField: 28 embeddedField: "embeddedValue" customField: "testName/testNamespace" newFieldName: true nestedField: embeddedField: "nestedValue" `) ) ================================================ FILE: pkg/yaml/yaml.go ================================================ // Package yaml handles the unmarshaling of YAML objects to strusts package yaml import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io" "reflect" "strings" "github.com/ghodss/yaml" "github.com/rancher/wrangler/v3/pkg/data/convert" "github.com/rancher/wrangler/v3/pkg/gvk" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" yamlDecoder "k8s.io/apimachinery/pkg/util/yaml" ) var ( cleanPrefix = []string{ "kubectl.kubernetes.io/", } cleanContains = []string{ "cattle.io/", } ) const buffSize = 4096 // Unmarshal decodes YAML bytes into document (as defined by // the YAML spec) then converting it to JSON via // "k8s.io/apimachinery/pkg/util/yaml".YAMLToJSON and then decoding the json in the the v interface{} func Unmarshal(data []byte, v interface{}) error { return yamlDecoder.NewYAMLToJSONDecoder(bytes.NewBuffer(data)).Decode(v) } // UnmarshalWithJSONDecoder expects a reader of raw YAML. // It converts the document or documents to JSON, // then decodes the JSON bytes into a slice of values of type T. // Type T must be a pointer, or the function will panic. func UnmarshalWithJSONDecoder[T any](yamlReader io.Reader) ([]T, error) { // verify the object is a pointer var obj T objPtrType := reflect.TypeOf(obj) if objPtrType.Kind() != reflect.Pointer { panic(fmt.Sprintf("Object T must be a pointer not %v", objPtrType)) } objType := objPtrType.Elem() var result []T // create a reader that reads one YAML document at a time. reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(yamlReader, buffSize)) for { // get raw yaml for a single document rawYAML, err := reader.Read() if errors.Is(err, io.EOF) { // finished reading break } if err != nil { return nil, fmt.Errorf("failed to read yaml: %w", err) } // convert YAML to JSON rawJSON, err := yamlDecoder.ToJSON(rawYAML) if err != nil { return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err) } // unmarshal JSON to the provided object newObj := reflect.New(objType).Interface().(T) err = json.Unmarshal(rawJSON, newObj) if err != nil { return nil, fmt.Errorf("failed to unmarshal converted JSON: %w", err) } result = append(result, newObj) } return result, nil } // ToObjects takes a reader of yaml bytes and returns a list of unstructured.Unstructured runtime.Objects that are read. // If one of the objects read is an unstructured.UnstructuredList then the list is flattened to individual objects. func ToObjects(in io.Reader) ([]runtime.Object, error) { var result []runtime.Object reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(in, buffSize)) for { raw, err := reader.Read() if err == io.EOF { break } if err != nil { return nil, err } obj, err := toObjects(raw) if err != nil { return nil, err } result = append(result, obj...) } return result, nil } func toObjects(bytes []byte) ([]runtime.Object, error) { bytes, err := yamlDecoder.ToJSON(bytes) if err != nil { return nil, err } check := map[string]interface{}{} if err := json.Unmarshal(bytes, &check); err != nil || len(check) == 0 { return nil, err } obj, _, err := unstructured.UnstructuredJSONScheme.Decode(bytes, nil, nil) if err != nil { return nil, err } if l, ok := obj.(*unstructured.UnstructuredList); ok { var result []runtime.Object for _, obj := range l.Items { copy := obj result = append(result, ©) } return result, nil } return []runtime.Object{obj}, nil } // Export will attempt to clean up the objects a bit before // rendering to yaml so that they can easily be imported into another // cluster func Export(objects ...runtime.Object) ([]byte, error) { if len(objects) == 0 { return nil, nil } buffer := &bytes.Buffer{} for i, obj := range objects { if i > 0 { buffer.WriteString("\n---\n") } obj, err := CleanObjectForExport(obj) if err != nil { return nil, err } bytes, err := yaml.Marshal(obj) if err != nil { return nil, fmt.Errorf("failed to encode %s: %w", obj.GetObjectKind().GroupVersionKind(), err) } buffer.Write(bytes) } return buffer.Bytes(), nil } func CleanObjectForExport(obj runtime.Object) (runtime.Object, error) { obj = obj.DeepCopyObject() if obj.GetObjectKind().GroupVersionKind().Kind == "" { if gvk, err := gvk.Get(obj); err == nil { obj.GetObjectKind().SetGroupVersionKind(gvk) } else if err != nil { return nil, fmt.Errorf("kind and/or apiVersion is not set on input object: %v: %w", obj, err) } } data, err := convert.EncodeToMap(obj) if err != nil { return nil, err } unstr := &unstructured.Unstructured{ Object: data, } metadata := map[string]interface{}{} if name := unstr.GetName(); len(name) > 0 { metadata["name"] = name } else if generated := unstr.GetGenerateName(); len(generated) > 0 { metadata["generateName"] = generated } else { return nil, fmt.Errorf("either name or generateName must be set on obj: %v", obj) } if unstr.GetNamespace() != "" { metadata["namespace"] = unstr.GetNamespace() } if annotations := unstr.GetAnnotations(); len(annotations) > 0 { cleanMap(annotations) if len(annotations) > 0 { metadata["annotations"] = annotations } else { delete(metadata, "annotations") } } if labels := unstr.GetLabels(); len(labels) > 0 { cleanMap(labels) if len(labels) > 0 { metadata["labels"] = labels } else { delete(metadata, "labels") } } if spec, ok := data["spec"]; ok { if spec == nil { delete(data, "spec") } else if m, ok := spec.(map[string]interface{}); ok && len(m) == 0 { delete(data, "spec") } } data["metadata"] = metadata delete(data, "status") return unstr, nil } func CleanAnnotationsForExport(annotations map[string]string) map[string]string { result := make(map[string]string, len(annotations)) outer: for k := range annotations { for _, prefix := range cleanPrefix { if strings.HasPrefix(k, prefix) { continue outer } } for _, contains := range cleanContains { if strings.Contains(k, contains) { continue outer } } result[k] = annotations[k] } return result } func cleanMap(annoLabels map[string]string) { for k := range annoLabels { for _, prefix := range cleanPrefix { if strings.HasPrefix(k, prefix) { delete(annoLabels, k) } } } } func ToBytes(objects []runtime.Object) ([]byte, error) { if len(objects) == 0 { return nil, nil } buffer := &bytes.Buffer{} for i, obj := range objects { if i > 0 { buffer.WriteString("\n---\n") } bytes, err := yaml.Marshal(obj) if err != nil { return nil, fmt.Errorf("failed to encode %s: %w", obj.GetObjectKind().GroupVersionKind(), err) } buffer.Write(bytes) } return buffer.Bytes(), nil } ================================================ FILE: pkg/yaml/yaml_test.go ================================================ package yaml import ( "bytes" "testing" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" ) func TestUnmarshalWithJSONDecoder_deployment(t *testing.T) { tests := []struct { name string input []byte want func() []*appsv1.Deployment wantErr bool }{ { name: "single deployment", input: singleDeployment, want: func() []*appsv1.Deployment { dep := &appsv1.Deployment{} dep.Name = "singleDeployment" dep.APIVersion = appsv1.SchemeGroupVersion.String() dep.Kind = "Deployment" three := int32(3) dep.Spec.Replicas = &three return []*appsv1.Deployment{dep} }, }, { name: "multiple deployment", input: multipleDeployments, want: func() []*appsv1.Deployment { dep := &appsv1.Deployment{} dep.Name = "dep1" dep.APIVersion = appsv1.SchemeGroupVersion.String() dep.Kind = "Deployment" three := int32(3) dep.Spec.Replicas = &three dep2 := &appsv1.Deployment{} dep2.APIVersion = appsv1.SchemeGroupVersion.String() dep2.Kind = "Deployment" dep2.Name = "dep2" dep2.Namespace = "dep2-ns" dep2.Spec.Paused = true dep3 := &appsv1.Deployment{} dep3.Name = "dep3" dep3.Namespace = "dep3-ns" dep3.Status.ReadyReplicas = 4 dep3.Labels = map[string]string{"app": "testapp"} return []*appsv1.Deployment{dep, dep2, dep3} }, }, { name: "empty document", input: emptyDoc, want: func() []*appsv1.Deployment { return nil }, }, { name: "unknown document", input: unknownDoc, want: func() []*appsv1.Deployment { dep := &appsv1.Deployment{} return []*appsv1.Deployment{dep} }, }, { name: "invalid YAML", input: invalidYAML, wantErr: true, }, { name: "invalid JSON marshal", input: singleString, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := UnmarshalWithJSONDecoder[*appsv1.Deployment](bytes.NewReader(tt.input)) if tt.wantErr { require.Error(t, err, "expected an error but got nil") return } require.NoError(t, err, "UnmarshalWithJSONDecoder received an unexpected error") var want []*appsv1.Deployment if tt.want != nil { want = tt.want() } require.Equal(t, got, want, "UnmarshalWithJSONDecoder received unexpected results") }) } } func TestUnmarshalWithJSONDecoder_jsonSruct(t *testing.T) { t.Parallel() tests := []struct { name string input []byte want func() []*JSONstruct wantErr bool }{ { name: "all expected fields", input: jsonYAML, want: func() []*JSONstruct { obj := &JSONstruct{} obj.EmbeddedField = "embeddedValue" obj.NestedField.EmbeddedField = "nestedValue" obj.CustomField.Name = "testName" obj.CustomField.Namespace = "testNamespace" obj.NormalField = 28 obj.MismatchField = true return []*JSONstruct{obj} }, }, { name: "unknown type", input: singleDeployment, want: func() []*JSONstruct { obj := &JSONstruct{} return []*JSONstruct{obj} }, }, { name: "empty document", input: emptyDoc, }, { name: "invalid YAML", input: invalidYAML, wantErr: true, }, { name: "invalid JSON marshal", input: singleString, wantErr: true, }, } for i := range tests { tt := &tests[i] t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := UnmarshalWithJSONDecoder[*JSONstruct](bytes.NewReader(tt.input)) if tt.wantErr { require.Error(t, err, "expected an error but got nil") return } require.NoError(t, err, "UnmarshalWithJSONDecoder received an unexpected error") var want []*JSONstruct if tt.want != nil { want = tt.want() } require.Equal(t, got, want, "UnmarshalWithJSONDecoder received unexpected results") }) } } ================================================ FILE: scripts/boilerplate.go.txt ================================================ /* Copyright 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. */ ================================================ FILE: scripts/ci ================================================ #! /bin/bash set -e cd $(dirname $0)/.. echo "Validating" if [[ -z $(type -p "mockgen") ]]; then echo "'mockgen': executable file not found in \$PATH. mockgen is needed to compelete code generation." echo "Install mockgen with 'go install go.uber.org/mock/mockgen@v0.6.0'" exit 1 fi echo Running: go generate go generate ./... echo Tidying up modules go mod tidy echo Verifying modules go mod verify if [ -n "$(git status --porcelain --untracked-files=no)" ]; then echo "Encountered dirty repo!" exit 1 fi echo "Running Test" go test ./...