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)
[]()
================================================
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 ./...